Merge "Taskbar All Apps Icon Shortcuts Popup Accesiiblity" into udc-dev
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index 55301ff..cebcd42 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -135,7 +135,7 @@
   }
 }
 
-// Next value 45
+// Next value 48
 enum Attribute {
   option allow_alias = true;
 
@@ -187,6 +187,11 @@
   ALL_APPS_SEARCH_RESULT_SYSTEM_POINTER = 42;
   ALL_APPS_SEARCH_RESULT_EDUCARD = 43;
 
+  // Result sources
+  DATA_SOURCE_APPSEARCH_APP_PREVIEW = 45;
+  DATA_SOURCE_APPSEARCH_APP_SRP_PREVIEW = 46;
+  DATA_SOURCE_AIAI_SEARCH_ROOT = 47;
+
   // Web suggestions provided by AGA
   ALL_APPS_SEARCH_RESULT_WEB_SUGGEST = 39;
 
diff --git a/quickstep/res/layout/transient_taskbar.xml b/quickstep/res/layout/transient_taskbar.xml
index 62c6933..bf4b811 100644
--- a/quickstep/res/layout/transient_taskbar.xml
+++ b/quickstep/res/layout/transient_taskbar.xml
@@ -38,6 +38,20 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
+    <com.android.launcher3.taskbar.bubbles.BubbleBarView
+        android:id="@+id/taskbar_bubbles"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/bubblebar_size"
+        android:layout_gravity="bottom|end"
+        android:layout_marginEnd="@dimen/transient_taskbar_bottom_margin"
+        android:layout_marginBottom="@dimen/transient_taskbar_bottom_margin"
+        android:paddingEnd="@dimen/taskbar_icon_spacing"
+        android:paddingStart="@dimen/taskbar_icon_spacing"
+        android:visibility="gone"
+        android:gravity="center"
+        android:clipChildren="false"
+        />
+
     <FrameLayout
         android:id="@+id/navbuttons_view"
         android:layout_width="match_parent"
@@ -74,10 +88,20 @@
     <com.android.launcher3.taskbar.StashedHandleView
         android:id="@+id/stashed_handle"
         tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
-        android:layout_width="match_parent"
+        android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:background="@color/taskbar_stashed_handle_dark_color"
         android:clipToOutline="true"
         android:layout_gravity="bottom"/>
 
+    <com.android.launcher3.taskbar.StashedHandleView
+        android:id="@+id/stashed_bubble_handle"
+        tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:background="@color/taskbar_stashed_handle_dark_color"
+        android:clipToOutline="true"
+        android:layout_gravity="bottom"/>
+
 </com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 6c7decd..5d2df70 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -292,6 +292,8 @@
     <dimen name="taskbar_stashed_small_screen">108dp</dimen>
     <dimen name="taskbar_unstash_input_area">316dp</dimen>
     <dimen name="taskbar_stashed_handle_height">4dp</dimen>
+    <dimen name="taskbar_stashed_screen_edge_hover_deadzone_height">10dp</dimen>
+    <dimen name="taskbar_stashed_below_hover_deadzone_height">1dp</dimen>
     <dimen name="taskbar_edu_horizontal_margin">112dp</dimen>
     <dimen name="taskbar_nav_buttons_width_kids">88dp</dimen>
     <dimen name="taskbar_nav_buttons_height_kids">40dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 45163cb..682fccd 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1086,7 +1086,7 @@
      * Adds remote animations to a {@link RemoteAnimationDefinition}. May be overridden to add
      * additional animations.
      */
-    protected void addRemoteAnimations(RemoteAnimationDefinition definition) {
+    private void addRemoteAnimations(RemoteAnimationDefinition definition) {
         mWallpaperOpenRunner = createWallpaperOpenRunner(false /* fromUnlock */);
         definition.addRemoteAnimation(WindowManager.TRANSIT_OLD_WALLPAPER_OPEN,
                 WindowConfiguration.ACTIVITY_TYPE_STANDARD,
@@ -1151,7 +1151,7 @@
         SystemUiProxy.INSTANCE.get(mLauncher).setStartingWindowListener(null);
     }
 
-    protected void unregisterRemoteAnimations() {
+    private void unregisterRemoteAnimations() {
         if (SEPARATE_RECENTS_ACTIVITY.get()) {
             return;
         }
@@ -1165,7 +1165,7 @@
         }
     }
 
-    private void unregisterRemoteTransitions() {
+    protected void unregisterRemoteTransitions() {
         if (ENABLE_SHELL_TRANSITIONS) {
             SystemUiProxy.INSTANCE.get(mLauncher).unshareTransactionQueue();
         }
@@ -1448,7 +1448,6 @@
      */
     private Animator getFallbackClosingWindowAnimators(RemoteAnimationTarget[] appTargets) {
         final int rotationChange = getRotationChange(appTargets);
-        SurfaceTransactionApplier surfaceApplier = new SurfaceTransactionApplier(mDragLayer);
         Matrix matrix = new Matrix();
         Point tmpPos = new Point();
         Rect tmpRect = new Rect();
@@ -1504,7 +1503,7 @@
                                 .setAlpha(1f);
                     }
                 }
-                surfaceApplier.scheduleApply(transaction);
+                transaction.getTransaction().apply();
             }
         });
 
@@ -1592,7 +1591,8 @@
             boolean playFallBackAnimation = (launcherView == null
                     && launcherIsForceInvisibleOrOpening)
                     || mLauncher.getWorkspace().isOverlayShown()
-                    || hasMultipleTargetsWithMode(appTargets, MODE_CLOSING);
+                    || hasMultipleTargetsWithMode(appTargets, MODE_CLOSING)
+                    || mLauncher.isDestroyed();
 
             boolean playWorkspaceReveal = true;
             boolean skipAllAppsScale = false;
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
index 268024f..885afff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
@@ -47,9 +47,7 @@
         mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
         mAllAppsButton = LayoutInflater.from(context)
                 .inflate(R.layout.taskbar_all_apps_button, mStartContextualContainer, false);
-        mAllAppsButton.setOnClickListener((View v) -> {
-            mControllers.taskbarAllAppsController.show();
-        });
+        mAllAppsButton.setOnClickListener(v -> mControllers.taskbarAllAppsController.toggle());
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 7e0530b..d94d8f7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -24,6 +24,7 @@
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY;
 import static com.android.launcher3.Utilities.isRunningInTestHarness;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING;
@@ -83,6 +84,12 @@
 import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSuspendFlag;
 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
+import com.android.launcher3.taskbar.bubbles.BubbleBarController;
+import com.android.launcher3.taskbar.bubbles.BubbleBarView;
+import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleStashController;
+import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
@@ -106,6 +113,7 @@
 import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider;
 
 import java.io.PrintWriter;
+import java.util.Optional;
 
 /**
  * The {@link ActivityContext} with which we inflate Taskbar-related Views. This allows UI elements
@@ -195,11 +203,23 @@
         TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
         FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
         StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
+        BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
+        StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);
 
         mAccessibilityDelegate = new TaskbarShortcutMenuAccessibilityDelegate(this);
 
         final boolean isDesktopMode = getPackageManager().hasSystemFeature(FEATURE_PC);
 
+        // If Bubble bar is present, TaskbarControllers depends on it so build it first.
+        Optional<BubbleControllers> bubbleControllersOptional = Optional.empty();
+        if (BubbleBarController.BUBBLE_BAR_ENABLED) {
+            bubbleControllersOptional = Optional.of(new BubbleControllers(
+                    new BubbleBarController(this, bubbleBarView),
+                    new BubbleBarViewController(this, bubbleBarView),
+                    new BubbleStashController(this),
+                    new BubbleStashedHandleViewController(this, bubbleHandleView)));
+        }
+
         // Construct controllers.
         mControllers = new TaskbarControllers(this,
                 new TaskbarDragController(this),
@@ -239,7 +259,8 @@
                         : TaskbarRecentAppsController.DEFAULT,
                 new TaskbarEduTooltipController(this),
                 new KeyboardQuickSwitchController(),
-                new TaskbarDividerPopupController(this));
+                new TaskbarDividerPopupController(this),
+                bubbleControllersOptional);
     }
 
     public void init(@NonNull TaskbarSharedState sharedState) {
@@ -661,7 +682,10 @@
     void onDragEndOrViewRemoved() {
         boolean isDragInProgress = mControllers.taskbarDragController.isSystemDragInProgress();
 
-        if (!isDragInProgress && !AbstractFloatingView.hasOpenView(this, TYPE_ALL)) {
+        // Overlay AFVs are in a separate window and do not require Taskbar to be fullscreen.
+        if (!isDragInProgress
+                && !AbstractFloatingView.hasOpenView(
+                        this, TYPE_ALL & ~TYPE_TASKBAR_OVERLAY_PROXY)) {
             // Reverts Taskbar window to its original size
             setTaskbarWindowFullscreen(false);
         }
@@ -773,6 +797,7 @@
     }
 
     protected void onTaskbarIconClicked(View view) {
+        boolean shouldCloseAllOpenViews = true;
         Object tag = view.getTag();
         if (tag instanceof Task) {
             Task task = (Task) tag;
@@ -780,6 +805,7 @@
                     ActivityOptions.makeBasic());
             mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
         } else if (tag instanceof FolderInfo) {
+            shouldCloseAllOpenViews = false;
             FolderIcon folderIcon = (FolderIcon) view;
             Folder folder = folderIcon.getFolder();
 
@@ -876,7 +902,9 @@
             Log.e(TAG, "Unknown type clicked: " + tag);
         }
 
-        AbstractFloatingView.closeAllOpenViews(this);
+        if (shouldCloseAllOpenViews) {
+            AbstractFloatingView.closeAllOpenViews(this);
+        }
     }
 
     /**
@@ -929,6 +957,13 @@
     }
 
     /**
+     * Returns whether the taskbar is currently visually stashed.
+     */
+    public boolean isTaskbarStashed() {
+        return mControllers.taskbarStashController.isStashed();
+    }
+
+    /**
      * Called when we detect a long press in the nav region before passing the gesture slop.
      * @return Whether taskbar handled the long press, and thus should cancel the gesture.
      */
@@ -972,10 +1007,23 @@
 
     /**
      * Called when we detect a motion down or up/cancel in the nav region while stashed.
+     *
      * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
      */
     public void startTaskbarUnstashHint(boolean animateForward) {
-        mControllers.taskbarStashController.startUnstashHint(animateForward);
+        // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
+        startTaskbarUnstashHint(animateForward, /* forceUnstash = */ false);
+    }
+
+    /**
+     * Called when we detect a motion down or up/cancel in the nav region while stashed.
+     *
+     * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
+     * @param forceUnstash Whether we force the unstash hint.
+     */
+    public void startTaskbarUnstashHint(boolean animateForward, boolean forceUnstash) {
+        // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
+        mControllers.taskbarStashController.startUnstashHint(animateForward, forceUnstash);
     }
 
     /**
@@ -1123,4 +1171,9 @@
     public int getTaskbarAllAppsScroll() {
         return mControllers.taskbarAllAppsController.getTaskbarAllAppsScroll();
     }
+
+    @VisibleForTesting
+    public float getStashedTaskbarScale() {
+        return mControllers.stashedHandleViewController.getStashedHandleHintScale().value;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 1cd6f50..66c2eb3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -23,12 +23,14 @@
 
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
 import com.android.systemui.shared.rotation.RotationButtonController;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Hosts various taskbar controllers to facilitate passing between one another.
@@ -61,6 +63,7 @@
     public final TaskbarEduTooltipController taskbarEduTooltipController;
     public final KeyboardQuickSwitchController keyboardQuickSwitchController;
     public final TaskbarDividerPopupController taskbarPinningController;
+    public final Optional<BubbleControllers> bubbleControllers;
 
     @Nullable private LoggableTaskbarController[] mControllersToLog = null;
     @Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -107,7 +110,8 @@
             TaskbarRecentAppsController taskbarRecentAppsController,
             TaskbarEduTooltipController taskbarEduTooltipController,
             KeyboardQuickSwitchController keyboardQuickSwitchController,
-            TaskbarDividerPopupController taskbarPinningController) {
+            TaskbarDividerPopupController taskbarPinningController,
+            Optional<BubbleControllers> bubbleControllers) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
         this.navButtonController = navButtonController;
@@ -133,6 +137,7 @@
         this.taskbarEduTooltipController = taskbarEduTooltipController;
         this.keyboardQuickSwitchController = keyboardQuickSwitchController;
         this.taskbarPinningController = taskbarPinningController;
+        this.bubbleControllers = bubbleControllers;
     }
 
     /**
@@ -167,6 +172,7 @@
         taskbarEduTooltipController.init(this);
         keyboardQuickSwitchController.init(this);
         taskbarPinningController.init(this);
+        bubbleControllers.ifPresent(controllers -> controllers.init(this));
 
         mControllersToLog = new LoggableTaskbarController[] {
                 taskbarDragController, navButtonController, navbarButtonsViewController,
@@ -226,6 +232,7 @@
         taskbarRecentAppsController.onDestroy();
         keyboardQuickSwitchController.onDestroy();
         taskbarStashController.onDestroy();
+        bubbleControllers.ifPresent(controllers -> controllers.onDestroy());
 
         mControllersToLog = null;
         mBackgroundRendererControllers = null;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index 41093bd..72add4f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -308,15 +308,12 @@
         // Pre-drag has ended, start the global system drag.
         if (mDisallowGlobalDrag) {
             AbstractFloatingView.closeAllOpenViewsExcept(mActivity, TYPE_TASKBAR_ALL_APPS);
-        } else {
-            AbstractFloatingView.closeAllOpenViews(mActivity);
+            return;
         }
-
         startSystemDrag((BubbleTextView) mDragObject.originalView);
     }
 
     private void startSystemDrag(BubbleTextView btv) {
-        if (mDisallowGlobalDrag) return;
         View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) {
 
             @Override
@@ -412,6 +409,9 @@
                         .log(StatsLogManager.LauncherEvent.LAUNCHER_ITEM_DRAG_STARTED);
             }
         }
+
+        // Wait to close until after system drag has started, if applicable.
+        AbstractFloatingView.closeAllOpenViews(mActivity);
     }
 
     private void onSystemDragStarted(BubbleTextView btv) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 19b9a18..4422fd4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -34,8 +34,6 @@
 import android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD
 import android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION
 import com.android.internal.policy.GestureNavigationSettingsObserver
-import com.android.launcher3.AbstractFloatingView
-import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY
 import com.android.launcher3.DeviceProfile
 import com.android.launcher3.R
 import com.android.launcher3.anim.AlphaUpdateListener
@@ -190,7 +188,7 @@
     /**
      * Called to update the touchable insets.
      *
-     * @see InternalInsetsInfo.setTouchableInsets
+     * @see ViewTreeObserver.InternalInsetsInfo.setTouchableInsets
      */
     fun updateInsetsTouchability(insetsInfo: ViewTreeObserver.InternalInsetsInfo) {
         insetsInfo.touchableRegion.setEmpty()
@@ -205,7 +203,7 @@
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
         } else if (
             controllers.navbarButtonsViewController.isImeVisible &&
-                controllers.taskbarStashController.isStashed()
+                controllers.taskbarStashController.isStashed
         ) {
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
         } else if (!controllers.uiController.isTaskbarTouchable) {
@@ -214,26 +212,16 @@
         } else if (controllers.taskbarDragController.isSystemDragInProgress) {
             // Let touches pass through us.
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
-        } else if (AbstractFloatingView.hasOpenView(context, TYPE_TASKBAR_OVERLAY_PROXY)) {
-            // Let touches pass through us if icons are hidden.
-            if (controllers.taskbarViewController.areIconsVisible()) {
-                insetsInfo.touchableRegion.set(touchableRegion)
-            }
-            insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
+        } else if (context.isTaskbarWindowFullscreen) {
+            // Intercept entire fullscreen window.
+            insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
+            insetsIsTouchableRegion = false
         } else if (
-            controllers.taskbarViewController.areIconsVisible() ||
-                AbstractFloatingView.hasOpenView(context, AbstractFloatingView.TYPE_ALL) ||
-                context.isNavBarKidsModeActive
+            controllers.taskbarViewController.areIconsVisible() || context.isNavBarKidsModeActive
         ) {
             // Taskbar has some touchable elements, take over the full taskbar area
-            insetsInfo.setTouchableInsets(
-                if (context.isTaskbarWindowFullscreen) {
-                    TOUCHABLE_INSETS_FRAME
-                } else {
-                    insetsInfo.touchableRegion.set(touchableRegion)
-                    TOUCHABLE_INSETS_REGION
-                }
-            )
+            insetsInfo.touchableRegion.set(touchableRegion)
+            insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
             insetsIsTouchableRegion = false
         } else {
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index ed78e2d..75cfd05 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -207,6 +207,10 @@
                     com.android.launcher3.taskbar.Utilities.setOverviewDragState(
                             mControllers, finalState.disallowTaskbarGlobalDrag(),
                             disallowLongClick, finalState.allowTaskbarInitialSplitSelection());
+                    // LauncherTaskbarUIController depends on the state when checking whether
+                    // to handle resume, so it should also be poked if current state changes
+                    mLauncher.getTaskbarUIController().onLauncherResumedOrPaused(
+                            mLauncher.hasBeenResumed());
                 }
             };
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index b2f9378..c2175f2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -856,15 +856,18 @@
     /**
      * Creates and starts a partial unstash animation, hinting at the new state that will trigger
      * when long press is detected.
+     *
      * @param animateForward Whether we are going towards the new unstashed state or returning to
      *                       the stashed state.
+     * @param forceUnstash Whether we force the unstash hint to animate.
      */
-    public void startUnstashHint(boolean animateForward) {
+    protected void startUnstashHint(boolean animateForward, boolean forceUnstash) {
         if (!isStashed()) {
             // Already unstashed, no need to hint in that direction.
             return;
         }
-        if (!canCurrentlyManuallyUnstash()) {
+        // TODO(b/270395798): Clean up after removing long-press unstashing code path.
+        if (!canCurrentlyManuallyUnstash() && !forceUnstash) {
             // If any other flags are causing us to be stashed, long press won't cause us to
             // unstash, so don't hint that it will.
             return;
@@ -931,20 +934,6 @@
     }
 
     /**
-     * Resets the flag if no system gesture is in progress.
-     * <p>
-     * Otherwise, the reset should be deferred until after the gesture is finished.
-     *
-     * @see #setSystemGestureInProgress
-     */
-    public void resetFlagIfNoGestureInProgress(int flag) {
-        if (!mIsSystemGestureInProgress) {
-            updateStateForFlag(flag, false);
-            applyState(mControllers.taskbarOverlayController.getCloseDuration());
-        }
-    }
-
-    /**
      * When hiding the IME, delay the unstash animation to align with the end of the transition.
      */
     private long getTaskbarStashStartDelayForIme() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
index 2373142..1cc6672 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -25,7 +25,7 @@
 import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.TouchController
-import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
 
 /**
  * A helper [TouchController] for [TaskbarDragLayerController], specifically to handle touch events
@@ -34,7 +34,7 @@
  *   or [MotionEvent.ACTION_OUTSIDE].
  * - Touches inside Transient Taskbar bounds will stash if it is detected as a swipe down gesture.
  *
- * Note: touches to *unstash* Taskbar are handled by [TaskbarStashInputConsumer].
+ * Note: touches to *unstash* Taskbar are handled by [TaskbarUnstashInputConsumer].
  */
 class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController {
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index a7e2daa..7429185 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -639,7 +639,7 @@
         public View.OnClickListener getAllAppsButtonClickListener() {
             return v -> {
                 mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_ALLAPPS_BUTTON_TAP);
-                mControllers.taskbarAllAppsController.show();
+                mControllers.taskbarAllAppsController.toggle();
             };
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index 4266c71..459a658 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -100,9 +100,13 @@
         }
     }
 
-    /** Opens the {@link TaskbarAllAppsContainerView} in a new window. */
-    public void show() {
-        show(true);
+    /** Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay window. */
+    public void toggle() {
+        if (isOpen()) {
+            mSlideInView.close(true);
+        } else {
+            show(true);
+        }
     }
 
     /** Returns {@code true} if All Apps is open. */
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
index 7a3b3e8..01342af 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsViewController.java
@@ -16,7 +16,6 @@
 package com.android.launcher3.taskbar.allapps;
 
 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_STASHED_IN_TASKBAR_ALL_APPS;
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -88,8 +87,10 @@
     }
 
     private void setUpTaskbarStashing() {
-        mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, true);
-        mTaskbarStashController.applyState(mOverlayController.getOpenDuration());
+        if (DisplayController.isTransientTaskbar(mContext)) {
+            mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, true);
+            mTaskbarStashController.applyState(mOverlayController.getOpenDuration());
+        }
 
         mNavbarButtonsViewController.setSlideInViewVisible(true);
         mSlideInView.setOnCloseBeginListener(() -> {
@@ -100,11 +101,6 @@
             if (DisplayController.isTransientTaskbar(mContext)) {
                 mTaskbarStashController.updateStateForFlag(FLAG_STASHED_IN_TASKBAR_ALL_APPS, false);
                 mTaskbarStashController.applyState(mOverlayController.getCloseDuration());
-            } else {
-                // Post in case view is closing due to gesture navigation. If a gesture is in
-                // progress, wait to unstash until after the gesture is finished.
-                MAIN_EXECUTOR.post(() -> mTaskbarStashController.resetFlagIfNoGestureInProgress(
-                        FLAG_STASHED_IN_TASKBAR_ALL_APPS));
             }
         });
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
index b1633e7..3cd5f75 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
@@ -30,7 +30,5 @@
     val appName: String
 ) {
 
-    fun getKey(): String {
-        return info.key
-    }
+    val key: String = info.key
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 228aabd..a466548 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -152,7 +152,9 @@
         mContext = context;
         mBarView = bubbleView; // Need the view for inflating bubble views.
 
-        // TODO: register the listener with SysUiProxu
+        if (BUBBLE_BAR_ENABLED) {
+            SystemUiProxy.INSTANCE.get(context).setBubblesListener(this);
+        }
         mMainExecutor = MAIN_EXECUTOR;
         mLauncherApps = context.getSystemService(LauncherApps.class);
         mIconFactory = new BubbleIconFactory(context,
@@ -164,7 +166,7 @@
     }
 
     public void onDestroy() {
-        // TODO: unregister the listener with SysUiProxy
+        SystemUiProxy.INSTANCE.get(mContext).setBubblesListener(null);
     }
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0afc2cb..82494c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.taskbar.TaskbarControllers;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.SystemUiProxy;
 
 import java.util.List;
 import java.util.Objects;
@@ -42,6 +43,7 @@
 
     private static final String TAG = BubbleBarViewController.class.getSimpleName();
 
+    private final SystemUiProxy mSystemUiProxy;
     private final TaskbarActivityContext mActivity;
     private final BubbleBarView mBarView;
     private final int mIconSize;
@@ -69,6 +71,7 @@
     public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
         mActivity = activity;
         mBarView = barView;
+        mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity);
         mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
         mBubbleBarAlpha.setUpdateVisibility(true);
         mIconSize = activity.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
@@ -101,7 +104,8 @@
             mBubbleStashController.stashBubbleBar();
         } else {
             mBubbleBarController.setSelectedBubble(bubble);
-            // TODO: Tell SysUi to show the expanded view for this bubble.
+            mSystemUiProxy.showBubble(bubble.getKey(),
+                    mBubbleStashController.isBubblesShowingOnHome());
         }
     }
 
@@ -270,11 +274,12 @@
         if (isExpanded != mBarView.isExpanded()) {
             mBarView.setExpanded(isExpanded);
             if (!isExpanded) {
-                // TODO: Tell SysUi to collapse the bubble
+                mSystemUiProxy.collapseBubbles();
             } else {
                 final String selectedKey = mBubbleBarController.getSelectedBubbleKey();
                 if (selectedKey != null) {
-                    // TODO: Tell SysUi to show the bubble
+                    mSystemUiProxy.showBubble(selectedKey,
+                            mBubbleStashController.isBubblesShowingOnHome());
                 } else {
                     Log.w(TAG, "trying to expand bubbles when there isn't one selected");
                 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
index fd22457..5902912 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
@@ -80,6 +80,12 @@
         return mOverlayController;
     }
 
+    /** Returns {@code true} if overlay or Taskbar windows are handling a system drag. */
+    boolean isAnySystemDragInProgress() {
+        return mDragController.isSystemDragInProgress()
+                || mTaskbarContext.getDragController().isSystemDragInProgress();
+    }
+
     @Override
     public DeviceProfile getDeviceProfile() {
         return mOverlayController.getLauncherDeviceProfile();
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
index 2c3e1ac..b4ec682 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayDragLayer.java
@@ -112,7 +112,7 @@
 
     @Override
     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
-        if (mActivity.getDragController().isSystemDragInProgress()) {
+        if (mActivity.isAnySystemDragInProgress()) {
             inoutInfo.touchableRegion.setEmpty();
             inoutInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
         }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
index e1ce9b1..b901a87 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsFragment.java
@@ -359,18 +359,6 @@
             return true;
         });
         sandboxCategory.addPreference(launchOverviewTutorialPreference);
-        Preference launchSandboxModeTutorialPreference = new Preference(context);
-        launchSandboxModeTutorialPreference.setKey("launchSandboxMode");
-        launchSandboxModeTutorialPreference.setTitle("Launch Sandbox Mode");
-        launchSandboxModeTutorialPreference.setSummary("Practice navigation gestures");
-        launchSandboxModeTutorialPreference.setOnPreferenceClickListener(preference -> {
-            startActivity(launchSandboxIntent
-                    .putExtra("use_tutorial_menu", false)
-                    .putExtra("tutorial_steps", new String[] {"SANDBOX_MODE"}));
-            return true;
-        });
-        sandboxCategory.addPreference(launchSandboxModeTutorialPreference);
-
         Preference launchSecondaryDisplayPreference = new Preference(context);
         launchSecondaryDisplayPreference.setKey("launchSecondaryDisplay");
         launchSecondaryDisplayPreference.setTitle("Launch Secondary Display");
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 5333cbe..7d47945 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -2114,12 +2114,11 @@
     }
 
     protected void linkRecentsViewScroll() {
-        SurfaceTransactionApplier.create(mRecentsView, applier -> {
-            runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
-                            .setSyncTransactionApplier(applier));
-            runOnRecentsAnimationAndLauncherBound(() ->
-                    mRecentsAnimationTargets.addReleaseCheck(applier));
-        });
+        SurfaceTransactionApplier applier = new SurfaceTransactionApplier(mRecentsView);
+        runActionOnRemoteHandles(remoteTargetHandle -> remoteTargetHandle.getTransformParams()
+                        .setSyncTransactionApplier(applier));
+        runOnRecentsAnimationAndLauncherBound(() ->
+                mRecentsAnimationTargets.addReleaseCheck(applier));
 
         mRecentsView.addOnScrollChangedListener(mOnRecentsScrollListener);
         runOnRecentsAnimationAndLauncherBound(() ->
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 64c9295..6b189cf 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -41,6 +41,7 @@
     int TYPE_ONE_HANDED = 1 << 11;
     int TYPE_TASKBAR_STASH = 1 << 12;
     int TYPE_STATUS_BAR = 1 << 13;
+    int TYPE_CURSOR_HOVER = 1 << 14;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -57,6 +58,7 @@
             "TYPE_ONE_HANDED",              // 11
             "TYPE_TASKBAR_STASH",           // 12
             "TYPE_STATUS_BAR",              // 13
+            "TYPE_CURSOR_HOVER",            // 14
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ab3ae9f..4c9cf8b 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -116,6 +116,16 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_STASHED_TASKBAR_SCALE: {
+                runOnTISBinder(tisBinder -> {
+                    response.putFloat(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                            tisBinder.getTaskbarManager()
+                                    .getCurrentActivityContext()
+                                    .getStashedTaskbarScale());
+                });
+                return response;
+            }
+
             case TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING: {
                 return getTISBinderUIProperty(Bundle::putInt, tisBinder ->
                         tisBinder.getTaskbarManager()
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 913f08f..d798e62 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -145,6 +145,9 @@
      * @param filter Returns true if GroupTask should be in the list of considerations
      */
     public void isTaskRemoved(int taskId, Consumer<Boolean> callback, Predicate<GroupTask> filter) {
+        // Invalidate the existing list before checking to ensure this reflects the current state in
+        // the system
+        mTaskList.onRecentTasksChanged();
         mTaskList.getTasks(true /* loadKeysOnly */, (taskGroups) -> {
             for (GroupTask group : taskGroups) {
                 if (group.containsTask(taskId)) {
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 36aa6f5..0549d9f 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -71,6 +71,8 @@
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
 import com.android.wm.shell.back.IBackAnimation;
+import com.android.wm.shell.bubbles.IBubbles;
+import com.android.wm.shell.bubbles.IBubblesListener;
 import com.android.wm.shell.desktopmode.IDesktopMode;
 import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
@@ -103,6 +105,7 @@
 
     private ISystemUiProxy mSystemUiProxy;
     private IPip mPip;
+    private IBubbles mBubbles;
     private ISysuiUnlockAnimationController mSysuiUnlockAnimationController;
     private ISplitScreen mSplitScreen;
     private IOneHanded mOneHanded;
@@ -121,6 +124,7 @@
     // up to the caller to clear the listeners to prevent leaks as these can be held indefinitely
     // in case SysUI needs to rebind.
     private IPipAnimationListener mPipAnimationListener;
+    private IBubblesListener mBubblesListener;
     private ISplitScreenListener mSplitScreenListener;
     private IStartingWindowListener mStartingWindowListener;
     private ILauncherUnlockAnimationController mLauncherUnlockAnimationController;
@@ -206,7 +210,7 @@
      * Sets proxy state, including death linkage, various listeners, and other configuration objects
      */
     @MainThread
-    public void setProxy(ISystemUiProxy proxy, IPip pip, ISplitScreen splitScreen,
+    public void setProxy(ISystemUiProxy proxy, IPip pip, IBubbles bubbles, ISplitScreen splitScreen,
             IOneHanded oneHanded, IShellTransitions shellTransitions,
             IStartingWindow startingWindow, IRecentTasks recentTasks,
             ISysuiUnlockAnimationController sysuiUnlockAnimationController,
@@ -216,6 +220,7 @@
         unlinkToDeath();
         mSystemUiProxy = proxy;
         mPip = pip;
+        mBubbles = bubbles;
         mSplitScreen = splitScreen;
         mOneHanded = oneHanded;
         mShellTransitions = shellTransitions;
@@ -229,6 +234,7 @@
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
         setPipAnimationListener(mPipAnimationListener);
+        setBubblesListener(mBubblesListener);
         registerSplitScreenListener(mSplitScreenListener);
         setStartingWindowListener(mStartingWindowListener);
         setLauncherUnlockAnimationController(mLauncherUnlockAnimationController);
@@ -244,7 +250,7 @@
      */
     @MainThread
     public void clearProxy() {
-        setProxy(null, null, null, null, null, null, null, null, null, null, null, null);
+        setProxy(null, null, null, null, null, null, null, null, null, null, null, null, null);
     }
 
     // TODO(141886704): Find a way to remove this
@@ -585,6 +591,59 @@
     }
 
     //
+    // Bubbles
+    //
+
+    /**
+     * Sets the listener to be notified of bubble state changes.
+     */
+    public void setBubblesListener(IBubblesListener listener) {
+        if (mBubbles != null) {
+            try {
+                if (mBubblesListener != null) {
+                    // Clear out any previous listener
+                    mBubbles.unregisterBubbleListener(mBubblesListener);
+                }
+                if (listener != null) {
+                    mBubbles.registerBubbleListener(listener);
+                }
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call registerBubblesListener");
+            }
+        }
+        mBubblesListener = listener;
+    }
+
+    /**
+     * Tells SysUI to show the bubble with the provided key.
+     * @param key the key of the bubble to show.
+     * @param onLauncherHome whether the bubble is showing on launcher home or not (modifies where
+     *                       the expanded bubble view is placed).
+     */
+    public void showBubble(String key, boolean onLauncherHome) {
+        if (mBubbles != null) {
+            try {
+                mBubbles.showBubble(key, onLauncherHome);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call showBubble");
+            }
+        }
+    }
+
+    /**
+     * Tells SysUI to collapse the bubbles.
+     */
+    public void collapseBubbles() {
+        if (mBubbles != null) {
+            try {
+                mBubbles.collapseBubbles();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call collapseBubbles");
+            }
+        }
+    }
+
+    //
     // Splitscreen
     //
 
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 6ea171e..682763f 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
+import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
@@ -42,6 +43,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BUBBLES;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
 import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED;
@@ -110,7 +112,7 @@
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.inputconsumers.StatusBarInputConsumer;
 import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
-import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
 import com.android.quickstep.util.ProtoTracer;
@@ -125,6 +127,7 @@
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.wm.shell.back.IBackAnimation;
+import com.android.wm.shell.bubbles.IBubbles;
 import com.android.wm.shell.desktopmode.IDesktopMode;
 import com.android.wm.shell.draganddrop.IDragAndDrop;
 import com.android.wm.shell.onehanded.IOneHanded;
@@ -168,6 +171,7 @@
             ISystemUiProxy proxy = ISystemUiProxy.Stub.asInterface(
                     bundle.getBinder(KEY_EXTRA_SYSUI_PROXY));
             IPip pip = IPip.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_PIP));
+            IBubbles bubbles = IBubbles.Stub.asInterface(bundle.getBinder(KEY_EXTRA_SHELL_BUBBLES));
             ISplitScreen splitscreen = ISplitScreen.Stub.asInterface(bundle.getBinder(
                     KEY_EXTRA_SHELL_SPLIT_SCREEN));
             IOneHanded onehanded = IOneHanded.Stub.asInterface(
@@ -191,7 +195,7 @@
                     bundle.getBinder(KEY_EXTRA_SHELL_DRAG_AND_DROP));
             MAIN_EXECUTOR.execute(() -> {
                 SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip,
-                        splitscreen, onehanded, shellTransitions, startingWindow,
+                        bubbles, splitscreen, onehanded, shellTransitions, startingWindow,
                         recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode,
                         unfoldTransition, dragAndDrop);
                 TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()");
@@ -641,12 +645,17 @@
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
 
         final int action = event.getActionMasked();
-        if (action == ACTION_DOWN) {
+        // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
+        // an ACTION_HOVER_ENTER will fire as well.
+        boolean isHoverActionWithoutConsumer =
+                event.isHoverEvent() && (mUncheckedConsumer.getType() & TYPE_CURSOR_HOVER) == 0;
+        if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
-            if (!mDeviceState.isOneHandedModeActive()
+            if ((!mDeviceState.isOneHandedModeActive()
                     && mRotationTouchHelper.isInSwipeUpTouchRegion(event,
-                    mOverviewComponentObserver.getActivityInterface())) {
+                    mOverviewComponentObserver.getActivityInterface()))
+                    || isHoverActionWithoutConsumer) {
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
@@ -723,6 +732,8 @@
             if (action == ACTION_POINTER_DOWN) {
                 mGestureState.setTrackpadGestureType(getTrackpadGestureType(event));
             }
+        } else if (event.isHoverEvent()) {
+            mUncheckedConsumer.onHoverEvent(event);
         } else {
             mUncheckedConsumer.onMotionEvent(event);
         }
@@ -846,7 +857,7 @@
                 base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
             }
 
-            // If Taskbar is present, we listen for long press to unstash it.
+            // If Taskbar is present, we listen for long press or cursor hover events to unstash it.
             TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
             if (tac != null) {
                 // Present always on large screen or on small screen w/ flag
@@ -857,8 +868,8 @@
                             .append(reasonPrefix)
                             .append(SUBSTRING_PREFIX)
                             .append("TaskbarActivityContext != null, "
-                                    + "using TaskbarStashInputConsumer");
-                    base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, tac);
+                                    + "using TaskbarUnstashInputConsumer");
+                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
                 }
             }
 
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 4b1dd43..074aedd 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -79,7 +79,7 @@
     }
 
     @Override
-    public void startHome() {
+    public void startHome(boolean animated) {
         mActivity.startHome();
         AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
     }
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
similarity index 69%
rename from quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 51c2b48..65c825c 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -19,17 +19,20 @@
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -40,9 +43,11 @@
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
- * Listens for a long press, and cancels the current gesture if that causes Taskbar to be unstashed.
+ * Listens for touch and hover events to unstash the Taskbar.
+ *
+ * <p>Cancels the current gesture if the long press causes the Taskbar to be unstashed.
  */
-public class TaskbarStashInputConsumer extends DelegateInputConsumer {
+public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
 
     private final TaskbarActivityContext mTaskbarActivityContext;
     private final GestureDetector mLongPressDetector;
@@ -64,9 +69,15 @@
 
     private final boolean mIsTransientTaskbar;
 
+    private boolean mIsStashedTaskbarHovered = false;
+    private final Rect mStashedTaskbarHandleBounds = new Rect();
+    private final Rect mBottomEdgeBounds = new Rect();
+    private final int mBottomScreenEdge;
+    private final int mStashedTaskbarBottomEdge;
+
     private final @Nullable TransitionCallback mTransitionCallback;
 
-    public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
+    public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
@@ -90,6 +101,11 @@
             }
         });
 
+        mBottomScreenEdge = res.getDimensionPixelSize(
+                R.dimen.taskbar_stashed_screen_edge_hover_deadzone_height);
+        mStashedTaskbarBottomEdge =
+                res.getDimensionPixelSize(R.dimen.taskbar_stashed_below_hover_deadzone_height);
+
         mTransitionCallback = mIsTransientTaskbar
                 ? taskbarActivityContext.getTranslationCallbacks()
                 : null;
@@ -97,7 +113,7 @@
 
     @Override
     public int getType() {
-        return TYPE_TASKBAR_STASH | mDelegate.getType();
+        return TYPE_TASKBAR_STASH | TYPE_CURSOR_HOVER | mDelegate.getType();
     }
 
     @Override
@@ -213,4 +229,73 @@
             }
         }
     }
+
+    /**
+     * Listen for hover events for the stashed taskbar.
+     *
+     * <p>When hovered over the stashed taskbar handle, show the unstash hint.
+     * <p>When the cursor is touching the bottom edge below the stashed taskbar, unstash it.
+     * <p>When the cursor is within a defined threshold of the screen's bottom edge outside of
+     * the stashed taskbar, unstash it.
+     */
+    @Override
+    public void onHoverEvent(MotionEvent ev) {
+        if (!ENABLE_CURSOR_HOVER_STATES.get() || mTaskbarActivityContext == null
+                || !mTaskbarActivityContext.isTaskbarStashed()) {
+            return;
+        }
+
+        if (mIsStashedTaskbarHovered) {
+            updateHoveredTaskbarState((int) ev.getX(), (int) ev.getY());
+        } else {
+            updateUnhoveredTaskbarState((int) ev.getX(), (int) ev.getY());
+        }
+    }
+
+    private void updateHoveredTaskbarState(int x, int y) {
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
+        mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
+
+        if (mBottomEdgeBounds.contains(x, y)) {
+            // If hovering stashed taskbar and then hover screen bottom edge, unstash it.
+            mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+            mIsStashedTaskbarHovered = false;
+        } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+            // If exit hovering stashed taskbar, remove hint.
+            startStashedTaskbarHover(/* isHovered = */ false);
+        }
+    }
+
+    private void updateUnhoveredTaskbarState(int x, int y) {
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        mBottomEdgeBounds.set(
+                0,
+                dp.heightPx - mBottomScreenEdge,
+                dp.widthPx,
+                dp.heightPx);
+
+        if (mStashedTaskbarHandleBounds.contains(x, y)) {
+            // If enter hovering stashed taskbar, start hint.
+            startStashedTaskbarHover(/* isHovered = */ true);
+        } else if (mBottomEdgeBounds.contains(x, y)) {
+            // If hover screen's bottom edge not below the stashed taskbar, unstash it.
+            mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+        }
+    }
+
+    private void startStashedTaskbarHover(boolean isHovered) {
+        mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
+        mIsStashedTaskbarHovered = isHovered;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
deleted file mode 100644
index f0bd4f9..0000000
--- a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialController.java
+++ /dev/null
@@ -1,70 +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 Sandbox Mode. */
-public class SandboxModeTutorialController extends SwipeUpGestureTutorialController {
-
-    SandboxModeTutorialController(SandboxModeTutorialFragment fragment, TutorialType tutorialType) {
-        super(fragment, tutorialType);
-    }
-
-    @Override
-    public void onBackGestureAttempted(BackGestureResult result) {
-        switch (result) {
-            case BACK_COMPLETED_FROM_LEFT:
-            case BACK_COMPLETED_FROM_RIGHT:
-                showRippleEffect(null);
-                showFeedback(R.string.sandbox_mode_back_gesture_feedback_successful);
-                break;
-            case BACK_CANCELLED_FROM_LEFT:
-            case BACK_CANCELLED_FROM_RIGHT:
-                showFeedback(R.string.back_gesture_feedback_cancelled);
-                break;
-            case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
-                showFeedback(R.string.sandbox_mode_back_gesture_feedback_swipe_too_far_from_edge);
-                break;
-        }
-    }
-
-    @Override
-    public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
-        switch (result) {
-            case HOME_GESTURE_COMPLETED:
-                animateFakeTaskViewHome(finalVelocity, () -> {
-                    showFeedback(R.string.sandbox_mode_home_gesture_feedback_successful);
-                });
-                break;
-            case OVERVIEW_GESTURE_COMPLETED:
-                fadeOutFakeTaskView(true, true, () -> {
-                    showFeedback(R.string.sandbox_mode_overview_gesture_feedback_successful);
-                });
-                break;
-            case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
-            case HOME_OR_OVERVIEW_CANCELLED:
-            case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
-            case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
-                showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
-                break;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.java
deleted file mode 100644
index 7bd52f7..0000000
--- a/quickstep/src/com/android/quickstep/interaction/SandboxModeTutorialFragment.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 general navigation gesture sandbox environment. */
-public class SandboxModeTutorialFragment extends TutorialFragment {
-
-    public SandboxModeTutorialFragment(boolean fromTutorialMenu) {
-        super(fromTutorialMenu);
-    }
-
-    @Override
-    TutorialController createController(TutorialType type) {
-        return new SandboxModeTutorialController(this, type);
-    }
-
-    @Override
-    Class<? extends TutorialController> getControllerClass() {
-        return SandboxModeTutorialController.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/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 6efdb07..d4ff457 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -829,9 +829,6 @@
         HOME_NAVIGATION,
         HOME_NAVIGATION_COMPLETE,
         OVERVIEW_NAVIGATION,
-        OVERVIEW_NAVIGATION_COMPLETE,
-        ASSISTANT,
-        ASSISTANT_COMPLETE,
-        SANDBOX_MODE
+        OVERVIEW_NAVIGATION_COMPLETE
     }
 }
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
index 25de605..9f15e19 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
@@ -117,8 +117,6 @@
             case OVERVIEW_NAVIGATION:
             case OVERVIEW_NAVIGATION_COMPLETE:
                 return new OverviewGestureTutorialFragment(fromTutorialMenu);
-            case SANDBOX_MODE:
-                return new SandboxModeTutorialFragment(fromTutorialMenu);
             default:
                 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
         }
diff --git a/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
index 95473dc..bb028a7 100644
--- a/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
+++ b/quickstep/src/com/android/quickstep/util/SurfaceTransactionApplier.java
@@ -22,13 +22,11 @@
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
 import android.view.ViewRootImpl;
 
 import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
 
-import java.util.function.Consumer;
-
-
 /**
  * Helper class to apply surface transactions in sync with RenderThread similar to
  *   android.view.SyncRtSurfaceTransactionApplier
@@ -39,22 +37,47 @@
 
     private static final int MSG_UPDATE_SEQUENCE_NUMBER = 0;
 
-    private final SurfaceControl mBarrierSurfaceControl;
-    private final ViewRootImpl mTargetViewRootImpl;
     private final Handler mApplyHandler;
 
+    private boolean mInitialized;
+    private SurfaceControl mBarrierSurfaceControl;
+    private ViewRootImpl mTargetViewRootImpl;
+
     private int mLastSequenceNumber = 0;
 
     /**
      * @param targetView The view in the surface that acts as synchronization anchor.
      */
     public SurfaceTransactionApplier(View targetView) {
-        mTargetViewRootImpl = targetView.getViewRootImpl();
-        mBarrierSurfaceControl = mTargetViewRootImpl.getSurfaceControl();
+        if (targetView.isAttachedToWindow()) {
+            initialize(targetView);
+        } else {
+            mInitialized = false;
+            targetView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    if (!mInitialized) {
+                        targetView.removeOnAttachStateChangeListener(this);
+                        initialize(targetView);
+                    }
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    // Do nothing
+                }
+            });
+        }
         mApplyHandler = new Handler(this::onApplyMessage);
         setCanRelease(true);
     }
 
+    private void initialize(View view) {
+        mTargetViewRootImpl = view.getViewRootImpl();
+        mBarrierSurfaceControl = mTargetViewRootImpl.getSurfaceControl();
+        mInitialized = true;
+    }
+
     protected boolean onApplyMessage(Message msg) {
         if (msg.what == MSG_UPDATE_SEQUENCE_NUMBER) {
             setCanRelease(msg.arg1 == mLastSequenceNumber);
@@ -70,6 +93,10 @@
      *               this method to avoid synchronization issues.
      */
     public void scheduleApply(SurfaceTransaction params) {
+        if (!mInitialized) {
+            params.getTransaction().apply();
+            return;
+        }
         View view = mTargetViewRootImpl.getView();
         if (view == null) {
             return;
@@ -93,33 +120,4 @@
         // Make sure a frame gets scheduled.
         view.invalidate();
     }
-
-    /**
-     * Creates an instance of SurfaceTransactionApplier, deferring until the target view is
-     * attached if necessary.
-     */
-    public static void create(
-            final View targetView, final Consumer<SurfaceTransactionApplier> callback) {
-        if (targetView == null) {
-            // No target view, no applier
-            callback.accept(null);
-        } else if (targetView.isAttachedToWindow()) {
-            // Already attached, we're good to go
-            callback.accept(new SurfaceTransactionApplier(targetView));
-        } else {
-            // Haven't been attached before we can get the view root
-            targetView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-                @Override
-                public void onViewAttachedToWindow(View v) {
-                    targetView.removeOnAttachStateChangeListener(this);
-                    callback.accept(new SurfaceTransactionApplier(targetView));
-                }
-
-                @Override
-                public void onViewDetachedFromWindow(View v) {
-                    // Do nothing
-                }
-            });
-        }
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
new file mode 100644
index 0000000..c22e0bc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskRemovedDuringLaunchListener.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util;
+
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
+import com.android.quickstep.RecentsModel;
+
+/**
+ * This class tracks the failure of a task launch through the TaskView.launchTask() call, in an
+ * edge case in which starting a new task may initially succeed (startActivity returns true), but
+ * the launch ultimately fails if the activity finishes while it is resuming.
+ *
+ * There are two signals this class checks, the launcher lifecycle and the transition completion.
+ * If we hit either of those signals and the task is no longer valid, then the registered failure
+ * callback will be notified.
+ */
+public class TaskRemovedDuringLaunchListener implements ActivityLifecycleCallbacksAdapter {
+
+    private Activity mActivity;
+    private int mLaunchedTaskId = INVALID_TASK_ID;
+    private Runnable mTaskLaunchFailedCallback = null;
+
+    /**
+     * Registers a failure listener callback if it detects a scenario in which an app launch
+     * failed before the transition finished.
+     */
+    public void register(Activity activity, int launchedTaskId,
+            @NonNull Runnable taskLaunchFailedCallback) {
+        activity.registerActivityLifecycleCallbacks(this);
+        mActivity = activity;
+        mLaunchedTaskId = launchedTaskId;
+        mTaskLaunchFailedCallback = taskLaunchFailedCallback;
+    }
+
+    /**
+     * Unregisters the failure listener.
+     */
+    private void unregister() {
+        mActivity.unregisterActivityLifecycleCallbacks(this);
+        mActivity = null;
+        mLaunchedTaskId = INVALID_TASK_ID;
+        mTaskLaunchFailedCallback = null;
+    }
+
+    /**
+     * Called when the transition finishes.
+     */
+    public void onTransitionFinished() {
+        // The transition finished and Launcher was not stopped, check if the launch failed
+        checkTaskLaunchFailed();
+    }
+
+    @Override
+    public void onActivityStopped(Activity activity) {
+        // The normal task launch case, Launcher stops and updates its state correctly
+        unregister();
+    }
+
+    @Override
+    public void onActivityResumed(Activity activity) {
+        // The transition hasn't finished but Launcher was resumed, check if the launch failed
+        checkTaskLaunchFailed();
+    }
+
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+        // If we somehow don't get any of the above signals, then just unregister this listener
+        unregister();
+    }
+
+    private void checkTaskLaunchFailed() {
+        if (mLaunchedTaskId != INVALID_TASK_ID) {
+            final int launchedTaskId = mLaunchedTaskId;
+            final Runnable taskLaunchFailedCallback = mTaskLaunchFailedCallback;
+            RecentsModel.INSTANCE.getNoCreate().isTaskRemoved(mLaunchedTaskId, (taskRemoved) -> {
+                if (taskRemoved) {
+                    ActiveGestureLog.INSTANCE.addLog("Launch failed, task (id=" + launchedTaskId
+                            + ") finished mid transition");
+                    taskLaunchFailedCallback.run();
+                }
+            }, (task) -> true /* filter */);
+            unregister();
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 379722b..1cfaf14 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -321,7 +321,7 @@
     }
 
     @Override
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         launchTasks();
         callback.accept(true);
     }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index 5bfd035..c6c84bd 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -237,9 +237,9 @@
     }
 
     @Override
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         getRecentsView().getSplitSelectController().launchTasks(mTask.key.id, mSecondaryTask.key.id,
-                SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList,
+                SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, isQuickswitch,
                 getSplitRatio());
     }
 
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 8ff0e9b..2008129 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -39,6 +39,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DesktopVisibilityController;
+import com.android.launcher3.statemanager.StateManager;
 import com.android.launcher3.statemanager.StateManager.StateListener;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.PendingSplitSelectInfo;
@@ -79,9 +80,11 @@
     }
 
     @Override
-    public void startHome() {
-        mActivity.getStateManager().goToState(NORMAL);
-        AbstractFloatingView.closeAllOpenViews(mActivity, mActivity.isStarted());
+    public void startHome(boolean animated) {
+        StateManager stateManager = mActivity.getStateManager();
+        animated &= stateManager.shouldAnimateStateChange();
+        stateManager.goToState(NORMAL, animated);
+        AbstractFloatingView.closeAllOpenViews(mActivity, animated);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index f0afa69..ff5af28 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2285,7 +2285,11 @@
         }
     }
 
-    public abstract void startHome();
+    public void startHome() {
+        startHome(mActivity.isStarted());
+    }
+
+    public abstract void startHome(boolean animated);
 
     public void reset() {
         setCurrentTask(-1);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 53660b5..50a1dad 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -92,6 +92,7 @@
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RemoteAnimationTargets;
 import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle;
+import com.android.quickstep.TaskAnimationManager;
 import com.android.quickstep.TaskIconCache;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
@@ -102,6 +103,7 @@
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.util.TaskRemovedDuringLaunchListener;
 import com.android.quickstep.util.TransformParams;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -801,6 +803,14 @@
                     recentsView.addSideTaskLaunchCallback(callbackList);
                     return callbackList;
                 }
+                if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
+                    // If the recents transition is running (ie. in live tile mode), then the start
+                    // of a new task will merge into the existing transition and it currently will
+                    // not be run independently, so we need to rely on the onTaskAppeared() call
+                    // for the new task to trigger the side launch callback to flush this runnable
+                    // list (which is usually flushed when the app launch animation finishes)
+                    recentsView.addSideTaskLaunchCallback(opts.onEndCallback);
+                }
                 return opts.onEndCallback;
             } else {
                 notifyTaskLaunchFailed(TAG);
@@ -815,23 +825,44 @@
      * Starts the task associated with this view without any animation
      */
     public void launchTask(@NonNull Consumer<Boolean> callback) {
-        launchTask(callback, false /* freezeTaskList */);
+        launchTask(callback, false /* isQuickswitch */);
     }
 
     /**
      * Starts the task associated with this view without any animation
      */
-    public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
+    public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
         if (mTask != null) {
             TestLogging.recordEvent(
                     TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", mTask);
 
+            final TaskRemovedDuringLaunchListener
+                    failureListener = new TaskRemovedDuringLaunchListener();
+            if (isQuickswitch) {
+                // We only listen for failures to launch in quickswitch because the during this
+                // gesture launcher is in the background state, vs other launches which are in
+                // the actual overview state
+                failureListener.register(mActivity, mTask.key.id, () -> {
+                    notifyTaskLaunchFailed(TAG);
+                    // Disable animations for now, as it is an edge case and the app usually covers
+                    // launcher and also any state transition animation also gets clobbered by
+                    // QuickstepTransitionManager.createWallpaperOpenAnimations when launcher
+                    // shows again
+                    getRecentsView().startHome(false /* animated */);
+                });
+            }
             // Indicate success once the system has indicated that the transition has started
-            ActivityOptions opts = makeCustomAnimation(getContext(), 0, 0,
-                    () -> callback.accept(true), MAIN_EXECUTOR.getHandler());
+            ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(getContext(), 0, 0,
+                    MAIN_EXECUTOR.getHandler(),
+                    elapsedRealTime -> {
+                        callback.accept(true);
+                    },
+                    elapsedRealTime -> {
+                        failureListener.onTransitionFinished();
+                    });
             opts.setLaunchDisplayId(
                     getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
-            if (freezeTaskList) {
+            if (isQuickswitch) {
                 opts.setFreezeRecentTasksReordering();
             }
             opts.setDisableStartingWindow(mSnapshotView.shouldShowSplashView());
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
index 20466ad..2c16c15 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
@@ -16,8 +16,10 @@
 package com.android.launcher3.taskbar
 
 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController
+import com.android.launcher3.taskbar.bubbles.BubbleControllers
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
 import com.android.systemui.shared.rotation.RotationButtonController
+import java.util.Optional
 import org.junit.Before
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
@@ -54,6 +56,7 @@
     @Mock lateinit var taskbarEduTooltipController: TaskbarEduTooltipController
     @Mock lateinit var keyboardQuickSwitchController: KeyboardQuickSwitchController
     @Mock lateinit var taskbarPinningController: TaskbarDividerPopupController
+    @Mock lateinit var optionalBubbleControllers: Optional<BubbleControllers>
 
     lateinit var taskbarControllers: TaskbarControllers
 
@@ -94,6 +97,7 @@
                 taskbarEduTooltipController,
                 keyboardQuickSwitchController,
                 taskbarPinningController,
+                optionalBubbleControllers,
             )
     }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index 6243471..f5c78f6 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -17,6 +17,7 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
@@ -264,6 +265,39 @@
                 .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE);
     }
 
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testShowTaskbarUnstashHintOnHover() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverToShowTaskbarUnstashHint();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testUnstashTaskbarOnScreenBottomEdgeHover() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverScreenBottomEdgeToUnstashTaskbar();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testHoverBelowHintedTaskbarToUnstash() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverBelowHintedTaskbarToUnstash();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     private Taskbar getTaskbar() {
         Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar();
         List<String> taskbarIconNames = taskbar.getIconNames();
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index acfd54c..65542cf 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -33,14 +33,10 @@
 import com.android.launcher3.statemanager.StateManager
 import com.android.launcher3.util.ComponentKey
 import com.android.launcher3.util.SplitConfigurationOptions
-import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
-import com.android.launcher3.util.mock
 import com.android.launcher3.util.withArgCaptor
 import com.android.quickstep.RecentsModel
 import com.android.quickstep.SystemUiProxy
 import com.android.systemui.shared.recents.model.Task
-import java.util.ArrayList
-import java.util.function.Consumer
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -50,7 +46,9 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
+import java.util.function.Consumer
 
 @RunWith(AndroidJUnit4::class)
 class SplitSelectStateControllerTest {
@@ -355,6 +353,7 @@
     @Test
     fun secondPendingIntentSet() {
         val itemInfo = ItemInfo()
+        `when`(pendingIntent.creatorUserHandle).thenReturn(primaryUserHandle)
         splitSelectStateController.setInitialTaskSelect(null, 0, itemInfo, null, 1)
         splitSelectStateController.setSecondTask(pendingIntent)
         assertTrue(splitSelectStateController.isBothSplitAppsConfirmed)
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 3f61aaa..6db4fb5 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -24,6 +24,7 @@
         android:layout_weight="1"
         android:importantForAccessibility="noHideDescendants"
         android:hapticFeedbackEnabled="false"
+        android:clipChildren="false"
         android:layout_marginVertical="8dp">
         <!-- The image of the widget. This view does not support padding. Any placement adjustment
              should be done using margins. Width & height are set at runtime after scaling the
diff --git a/res/layout/widgets_bottom_sheet_content.xml b/res/layout/widgets_bottom_sheet_content.xml
index b76eef7..065c2ed 100644
--- a/res/layout/widgets_bottom_sheet_content.xml
+++ b/res/layout/widgets_bottom_sheet_content.xml
@@ -42,7 +42,7 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:fadeScrollbars="false"
-            android:layout_marginVertical="16dp">
+            android:layout_marginTop="16dp">
             <include layout="@layout/widgets_table_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 5fbd48c..f041ffb 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, ModelDbController controller,
+    public static void restoreAppWidgetIds(Context context, DatabaseHelper helper,
             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(controller, where, args))
+                            new ContentWriter.CommitParams(helper, where, args))
                     .put(LauncherSettings.Favorites.APPWIDGET_ID, newWidgetIds[i])
                     .put(LauncherSettings.Favorites.RESTORED, state)
                     .commit();
             if (result == 0) {
-                Cursor cursor = controller.getDb().query(
+                Cursor cursor = helper.getWritableDatabase().query(
                         Favorites.TABLE_NAME,
                         new String[] {Favorites.APPWIDGET_ID},
                         "appWidgetId=?", new String[] { oldWidgetId }, null, null, null);
diff --git a/src/com/android/launcher3/DropTargetHandler.kt b/src/com/android/launcher3/DropTargetHandler.kt
index 277f8b3..6560e16 100644
--- a/src/com/android/launcher3/DropTargetHandler.kt
+++ b/src/com/android/launcher3/DropTargetHandler.kt
@@ -6,7 +6,6 @@
 import com.android.launcher3.SecondaryDropTarget.DeferredOnComplete
 import com.android.launcher3.dragndrop.DragLayer
 import com.android.launcher3.logging.StatsLogManager.LauncherEvent
-import com.android.launcher3.model.ModelWriter
 import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.model.data.LauncherAppWidgetInfo
 import com.android.launcher3.util.IntSet
@@ -22,8 +21,6 @@
 class DropTargetHandler(launcher: Launcher) {
     val mLauncher: Launcher = launcher
 
-    val modelWriter: ModelWriter = mLauncher.modelWriter
-
     fun onDropAnimationComplete() {
         mLauncher.stateManager.goToState(LauncherState.NORMAL)
     }
@@ -87,7 +84,7 @@
             else mLauncher.workspace.currentPageScreenIds
         val onUndoClicked = Runnable {
             mLauncher.setPagesToBindSynchronously(pageIds)
-            modelWriter.abortDelete()
+            mLauncher.modelWriter.abortDelete()
             mLauncher.statsLogManager.logger().log(LauncherEvent.LAUNCHER_UNDO)
         }
 
@@ -95,7 +92,7 @@
             mLauncher,
             R.string.item_removed,
             R.string.undo,
-            modelWriter::commitDelete,
+            mLauncher.modelWriter::commitDelete,
             onUndoClicked
         )
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4764d72..29b0f08 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1621,6 +1621,9 @@
         return mModel;
     }
 
+    /**
+     * Returns the ModelWriter writer, make sure to call the function every time you want to use it.
+     */
     public ModelWriter getModelWriter() {
         return mModelWriter;
     }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 9abec50..0df4bd4 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -263,6 +263,18 @@
                 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 7fda326..b65e96b 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -148,6 +148,11 @@
         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";
@@ -159,6 +164,18 @@
                 + 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.
@@ -359,6 +376,10 @@
 
         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";
@@ -372,8 +393,11 @@
         }
 
         public static Bundle call(ContentResolver cr, String method, String arg) {
-            return cr.call(CONTENT_URI, method, arg, null);
+            return call(cr, method, arg, null /* extras */);
         }
 
+        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/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index fef6639..065122a 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -253,7 +253,7 @@
             "COLLECT_SEARCH_HISTORY", DISABLED, "Allow launcher to collect search history for log");
 
     public static final BooleanFlag ENABLE_TWOLINE_ALLAPPS = getDebugFlag(270390937,
-            "ENABLE_TWOLINE_ALLAPPS", DISABLED, "Enables two line label inside all apps.");
+            "ENABLE_TWOLINE_ALLAPPS", TEAMFOOD, "Enables two line label inside all apps.");
 
     public static final BooleanFlag IME_STICKY_SNACKBAR_EDU = getDebugFlag(270391693,
             "IME_STICKY_SNACKBAR_EDU", ENABLED, "Show sticky IME edu in AllApps");
@@ -415,7 +415,7 @@
 
     // TODO(Block 31)
     public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325,
-            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", DISABLED,
+            "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", ENABLED,
             "Use refactored split launching code path");
 
     // TODO(Block 32): Empty block
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 4906c1d..00f4285 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -304,7 +304,8 @@
         mWidgetOptions = pendingInfo.getDefaultSizeOptions(this);
         mWidgetCell.getWidgetView().setTag(pendingInfo);
 
-        applyWidgetItemAsync(() -> new WidgetItem(widgetInfo, mIdp, mApp.getIconCache()));
+        applyWidgetItemAsync(() -> new WidgetItem(
+                widgetInfo, mIdp, mApp.getIconCache(), mApp.getContext()));
         return WidgetsModel.newPendingItemInfo(this, widgetInfo.getComponent(),
                 widgetInfo.getUser());
     }
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index 8f0b8ec..372e9bf 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -16,7 +16,6 @@
 
 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;
 
@@ -53,8 +52,6 @@
 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;
@@ -148,9 +145,7 @@
         final String query = LauncherSettings.Favorites.ITEM_TYPE + " = "
                 + LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 
-        ModelDbController mainController =
-                LauncherAppState.getInstance(mContext).getModel().getModelDbController();
-        try (Cursor c = mainController.query(TABLE_NAME,
+        try (Cursor c = context.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
                 new String[] {
                         LauncherSettings.Favorites.APPWIDGET_ID,
                         LauncherSettings.Favorites.SPANX,
@@ -195,6 +190,8 @@
 
     @WorkerThread
     private void loadModelData() {
+        final boolean migrated = doGridMigrationIfNecessary();
+
         final Context inflationContext;
         if (mWallpaperColors != null) {
             // Create a themed context, without affecting the main application context
@@ -212,20 +209,8 @@
                     Themes.getActivityThemeRes(mContext));
         }
 
-        if (GridSizeMigrationUtil.needsToMigrate(inflationContext, mIdp)) {
-            // Start the migration
+        if (migrated) {
             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,
@@ -244,7 +229,8 @@
                         query += " or " + LauncherSettings.Favorites.SCREEN + " = "
                                 + Workspace.SECOND_SCREEN_ID;
                     }
-                    loadWorkspace(new ArrayList<>(), query, null);
+                    loadWorkspaceForPreviewSurfaceRenderer(new ArrayList<>(),
+                            LauncherSettings.Favorites.PREVIEW_CONTENT_URI, query);
 
                     final SparseArray<Size> spanInfo =
                             getLoadedLauncherWidgetInfo(previewContext.getBaseContext());
@@ -267,6 +253,14 @@
         }
     }
 
+    @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
index 27b8c3b..93e9de5 100644
--- a/src/com/android/launcher3/logging/StartupLatencyLogger.kt
+++ b/src/com/android/launcher3/logging/StartupLatencyLogger.kt
@@ -21,14 +21,13 @@
         const val UNSET_LONG = -1L
     }
 
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     val startTimeByEvent = SparseLongArray()
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     val endTimeByEvent = SparseLongArray()
 
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-    var cardinality: Int = UNSET_INT
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) var cardinality: Int = UNSET_INT
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
     var workspaceLoadStartTime: Long = UNSET_LONG
 
     private var isInTest = false
diff --git a/src/com/android/launcher3/model/DatabaseHelper.java b/src/com/android/launcher3/model/DatabaseHelper.java
index ecf5f67..dc5fcf7 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,6 +36,9 @@
 
 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;
@@ -55,7 +58,6 @@
 import java.net.URISyntaxException;
 import java.util.Arrays;
 import java.util.Locale;
-import java.util.function.ToLongFunction;
 import java.util.stream.Collectors;
 
 /**
@@ -74,23 +76,45 @@
     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 ToLongFunction<UserHandle> mUserSerialProvider;
-    private final Runnable mOnEmptyDbCreateCallback;
-
+    private final boolean mForMigration;
     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,
-            ToLongFunction<UserHandle> userSerialProvider, Runnable onEmptyDbCreateCallback) {
+    public DatabaseHelper(Context context, String dbName, boolean forMigration) {
         super(context, dbName, SCHEMA_VERSION);
         mContext = context;
-        mUserSerialProvider = userSerialProvider;
-        mOnEmptyDbCreateCallback = onEmptyDbCreateCallback;
+        mForMigration = forMigration;
     }
 
     protected void initIds() {
@@ -107,11 +131,13 @@
 
         mMaxItemId = 1;
 
-        addTableToDb(db, getDefaultUserSerial(), false /* optional */);
+        addFavoritesTable(db, false);
 
         // Fresh and clean launcher DB.
         mMaxItemId = initializeMaxItemId(db);
-        mOnEmptyDbCreateCallback.run();
+        if (!mForMigration) {
+            onEmptyDbCreated();
+        }
     }
 
     public void onAddOrDeleteOp(SQLiteDatabase db) {
@@ -121,8 +147,38 @@
         }
     }
 
-    private long getDefaultUserSerial() {
-        return mUserSerialProvider.applyAsLong(Process.myUserHandle());
+    /**
+     * 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);
     }
 
     @Override
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index 9a6cde6..eded5ea 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -16,9 +16,6 @@
 
 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;
@@ -37,15 +34,16 @@
 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;
 
@@ -91,38 +89,81 @@
         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, we copy the table
-     * {@link LauncherSettings.Favorites#TABLE_NAME} from {@code source} into
-     * {@link LauncherSettings.Favorites#TMP_TABLE}, run the grid size migration algorithm
+     * 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
      * 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(
-            @NonNull Context context,
-            @NonNull InvariantDeviceProfile idp,
-            @NonNull DatabaseHelper target,
-            @NonNull SQLiteDatabase source) {
+    public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
+        boolean migrateForPreview = idp != null;
+        if (!migrateForPreview) {
+            idp = LauncherAppState.getIDP(context);
+        }
 
         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 = new SQLiteTransaction(target.getWritableDatabase())) {
-            DbReader srcReader = new DbReader(t.getDb(), TMP_TABLE, context, validPackages);
-            DbReader destReader = new DbReader(t.getDb(), TABLE_NAME, context, validPackages);
+        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);
 
             Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows());
-            migrate(target, srcReader, destReader, destDeviceState.getNumHotseat(),
+            migrate(context, t.getDb(), srcReader, destReader, destDeviceState.getNumHotseat(),
                     targetSize, srcDeviceState, destDeviceState);
-            dropTable(t.getDb(), TMP_TABLE);
+
+            if (!migrateForPreview) {
+                dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
+            }
+
             t.commit();
             return true;
         } catch (Exception e) {
@@ -133,7 +174,7 @@
             Log.v(TAG, "Workspace migration completed in "
                     + (System.currentTimeMillis() - migrationStartTime));
 
-            if (!(context instanceof SandboxContext)) {
+            if (!migrateForPreview) {
                 // Save current configuration, so that the migration does not run again.
                 destDeviceState.writeToPrefs(context);
             }
@@ -141,7 +182,7 @@
     }
 
     public static boolean migrate(
-            @NonNull DatabaseHelper helper,
+            @NonNull final Context context, @NonNull final SQLiteDatabase db,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
             final int destHotseatSize, @NonNull final Point targetSize,
             @NonNull final DeviceGridState srcDeviceState,
@@ -193,8 +234,8 @@
         Collections.sort(workspaceToBeAdded);
 
         // Migrate hotseat
-        solveHotseatPlacement(helper, destHotseatSize,
-                srcReader, destReader, dstHotseatItems, hotseatToBeAdded);
+        solveHotseatPlacement(db, srcReader,
+                destReader, context, destHotseatSize, dstHotseatItems, hotseatToBeAdded);
 
         // Migrate workspace.
         // First we create a collection of the screens
@@ -214,8 +255,8 @@
             if (DEBUG) {
                 Log.d(TAG, "Migrating " + screenId);
             }
-            solveGridPlacement(helper, srcReader,
-                    destReader, screenId, trgX, trgY, workspaceToBeAdded, false);
+            solveGridPlacement(db, srcReader,
+                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, false);
             if (workspaceToBeAdded.isEmpty()) {
                 break;
             }
@@ -225,8 +266,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(helper, srcReader,
-                    destReader, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
+            solveGridPlacement(db, srcReader,
+                    destReader, context, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
             screenId++;
         }
 
@@ -257,33 +298,33 @@
         });
     }
 
-    private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry,
+    private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry,
             String srcTableName, String destTableName) {
-        int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName);
+        int id = copyEntryAndUpdate(db, context, entry, srcTableName, destTableName);
 
         if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
             for (Set<Integer> itemIds : entry.mFolderItems.values()) {
                 for (int itemId : itemIds) {
-                    copyEntryAndUpdate(helper, itemId, id, srcTableName, destTableName);
+                    copyEntryAndUpdate(db, context, itemId, id, srcTableName, destTableName);
                 }
             }
         }
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper,
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
             DbEntry entry, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, entry, -1, -1, srcTableName, destTableName);
+        return copyEntryAndUpdate(db, context, entry, -1, -1, srcTableName, destTableName);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper,
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
             int id, int folderId, String srcTableName, String destTableName) {
-        return copyEntryAndUpdate(helper, null, id, folderId, srcTableName, destTableName);
+        return copyEntryAndUpdate(db, context, null, id, folderId, srcTableName, destTableName);
     }
 
-    private static int copyEntryAndUpdate(DatabaseHelper helper, DbEntry entry,
-            int id, int folderId, String srcTableName, String destTableName) {
+    private static int copyEntryAndUpdate(SQLiteDatabase db, Context context,
+            DbEntry entry, int id, int folderId, String srcTableName, String destTableName) {
         int newId = -1;
-        Cursor c = helper.getWritableDatabase().query(srcTableName, null,
+        Cursor c = db.query(srcTableName, null,
                 LauncherSettings.Favorites._ID + " = '" + (entry != null ? entry.id : id) + "'",
                 null, null, null, null);
         while (c.moveToNext()) {
@@ -294,9 +335,11 @@
             } else {
                 values.put(LauncherSettings.Favorites.CONTAINER, folderId);
             }
-            newId = helper.generateNewItemId();
+            newId = LauncherSettings.Settings.call(context.getContentResolver(),
+                    LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
+                    LauncherSettings.Settings.EXTRA_VALUE);
             values.put(LauncherSettings.Favorites._ID, newId);
-            helper.getWritableDatabase().insert(destTableName, null, values);
+            db.insert(destTableName, null, values);
         }
         c.close();
         return newId;
@@ -324,9 +367,9 @@
         return validPackages;
     }
 
-    private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
+    private static void solveGridPlacement(@NonNull final SQLiteDatabase db,
             @NonNull final DbReader srcReader, @NonNull final DbReader destReader,
-            final int screenId, final int trgX, final int trgY,
+            @NonNull final Context context, 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);
@@ -348,7 +391,7 @@
                 continue;
             }
             if (findPlacementForEntry(entry, next, trg, occupied, screenId)) {
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(db, context, entry, srcReader.mTableName, destReader.mTableName);
                 iterator.remove();
             }
         }
@@ -385,9 +428,9 @@
         return false;
     }
 
-    private static void solveHotseatPlacement(
-            @NonNull final DatabaseHelper helper, final int hotseatSize,
+    private static void solveHotseatPlacement(@NonNull final SQLiteDatabase db,
             @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) {
 
@@ -404,7 +447,7 @@
                 // to something other than -1.
                 entry.cellX = i;
                 entry.cellY = 0;
-                insertEntryInDb(helper, entry, srcReader.mTableName, destReader.mTableName);
+                insertEntryInDb(db, context, 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 2054d93..a5dccc1 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,7 +66,9 @@
     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;
 
@@ -106,14 +108,17 @@
     public int itemType;
     public int restoreFlag;
 
-    public LoaderCursor(Cursor cursor, LauncherAppState app, UserManagerState userManagerState) {
+    public LoaderCursor(Cursor cursor, Uri contentUri, 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);
@@ -385,7 +390,7 @@
      */
     public ContentWriter updater() {
        return new ContentWriter(mContext, new ContentWriter.CommitParams(
-               mApp.getModel().getModelDbController(),
+               mApp.getModel().getModelDbController().getDatabaseHelper(),
                BaseColumns._ID + "= ?", new String[]{Integer.toString(id)}));
     }
 
@@ -404,8 +409,8 @@
     public boolean commitDeleted() {
         if (mItemsToRemove.size() > 0) {
             // Remove dead items
-            mApp.getModel().getModelDbController().delete(TABLE_NAME,
-                    Utilities.createDbSelectionQuery(Favorites._ID, mItemsToRemove), null);
+            mContext.getContentResolver().delete(mContentUri, Utilities.createDbSelectionQuery(
+                    Favorites._ID, mItemsToRemove), null);
             return true;
         }
         return false;
@@ -430,8 +435,9 @@
             // Update restored items that no longer require special handling
             ContentValues values = new ContentValues();
             values.put(Favorites.RESTORED, 0);
-            mApp.getModel().getModelDbController().update(TABLE_NAME, values,
-                    Utilities.createDbSelectionQuery(Favorites._ID, mRestoredRows), null);
+            mContext.getContentResolver().update(mContentUri, 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 d4eded5..9053d19 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -16,7 +16,6 @@
 
 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;
@@ -42,6 +41,7 @@
 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,6 +50,7 @@
 import android.util.ArrayMap;
 import android.util.Log;
 import android.util.LongSparseArray;
+import android.util.TimingLogger;
 
 import androidx.annotation.Nullable;
 
@@ -199,10 +200,25 @@
         }
 
         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<>();
-            loadWorkspace(allShortcuts, "", memoryLogger);
+            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");
+            }
 
             // 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
@@ -212,21 +228,21 @@
                 verifyNotStopped();
                 sanitizeFolders(mItemsDeleted);
                 sanitizeWidgetsShortcutsAndPackages();
-                logASplit("sanitizeData");
+                logASplit(timingLogger, "sanitizeData");
             }
 
             verifyNotStopped();
             mLauncherBinder.bindWorkspace(true /* incrementBindId */, /* isBindSync= */ false);
-            logASplit("bindWorkspace");
+            logASplit(timingLogger, "bindWorkspace");
 
             mModelDelegate.workspaceLoadComplete();
             // Notify the installer packages of packages with active installs on the first screen.
             sendFirstScreenActiveInstallsBroadcast();
-            logASplit("sendFirstScreenActiveInstallsBroadcast");
+            logASplit(timingLogger, "sendFirstScreenActiveInstallsBroadcast");
 
             // Take a break
             waitForIdle();
-            logASplit("step 1 complete");
+            logASplit(timingLogger, "step 1 complete");
             verifyNotStopped();
 
             // second step
@@ -237,16 +253,16 @@
             } finally {
                 Trace.endSection();
             }
-            logASplit("loadAllApps");
+            logASplit(timingLogger, "loadAllApps");
 
             if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
                         mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
-                logASplit("allAppsDelegateItems");
+                logASplit(timingLogger, "allAppsDelegateItems");
             }
             verifyNotStopped();
             mLauncherBinder.bindAllApps();
-            logASplit("bindAllApps");
+            logASplit(timingLogger, "bindAllApps");
 
             verifyNotStopped();
             IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
@@ -254,73 +270,75 @@
             updateHandler.updateIcons(allActivityList,
                     LauncherActivityCachingLogic.newInstance(mApp.getContext()),
                     mApp.getModel()::onPackageIconsUpdated);
-            logASplit("update icon cache");
+            logASplit(timingLogger, "update icon cache");
 
             verifyNotStopped();
-            logASplit("save shortcuts in icon cache");
+            logASplit(timingLogger, "save shortcuts in icon cache");
             updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
                     mApp.getModel()::onPackageIconsUpdated);
 
             // Take a break
             waitForIdle();
-            logASplit("step 2 complete");
+            logASplit(timingLogger, "step 2 complete");
             verifyNotStopped();
 
             // third step
             List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
-            logASplit("loadDeepShortcuts");
+            logASplit(timingLogger, "loadDeepShortcuts");
 
             verifyNotStopped();
             mLauncherBinder.bindDeepShortcuts();
-            logASplit("bindDeepShortcuts");
+            logASplit(timingLogger, "bindDeepShortcuts");
 
             verifyNotStopped();
-            logASplit("save deep shortcuts in icon cache");
+            logASplit(timingLogger, "save deep shortcuts in icon cache");
             updateHandler.updateIcons(allDeepShortcuts,
                     new ShortcutCachingLogic(), (pkgs, user) -> { });
 
             // Take a break
             waitForIdle();
-            logASplit("step 3 complete");
+            logASplit(timingLogger, "step 3 complete");
             verifyNotStopped();
 
             // fourth step
             List<ComponentWithLabelAndIcon> allWidgetsList =
                     mBgDataModel.widgetsModel.update(mApp, null);
-            logASplit("load widgets");
+            logASplit(timingLogger, "load widgets");
 
             verifyNotStopped();
             mLauncherBinder.bindWidgets();
-            logASplit("bindWidgets");
+            logASplit(timingLogger, "bindWidgets");
             verifyNotStopped();
 
             if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
-                logASplit("otherDelegateItems");
+                logASplit(timingLogger, "otherDelegateItems");
                 verifyNotStopped();
             }
 
             updateHandler.updateIcons(allWidgetsList,
                     new ComponentWithIconCachingLogic(mApp.getContext(), true),
                     mApp.getModel()::onWidgetLabelsUpdated);
-            logASplit("save widgets in icon cache");
+            logASplit(timingLogger, "save widgets in icon cache");
 
             // fifth step
             loadFolderNames();
 
             verifyNotStopped();
             updateHandler.finish();
-            logASplit("finish icon update");
+            logASplit(timingLogger, "finish icon update");
 
             mModelDelegate.modelLoadComplete();
             transaction.commit();
             memoryLogger.clearLogs();
         } catch (CancellationException e) {
             // Loader stopped, ignore
-            logASplit("Cancelled");
+            logASplit(timingLogger, "Cancelled");
         } catch (Exception e) {
             memoryLogger.printLogs();
             throw e;
+        } finally {
+            timingLogger.dumpToLog();
         }
         TraceHelper.INSTANCE.endSection(traceToken);
     }
@@ -330,29 +348,25 @@
         this.notify();
     }
 
-    protected void loadWorkspace(
-            List<ShortcutInfo> allDeepShortcuts,
-            String selection,
-            LoaderMemoryLogger memoryLogger) {
-        Trace.beginSection("LoadWorkspace");
-        try {
-            loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger);
-        } finally {
-            Trace.endSection();
-        }
-        logASplit("loadWorkspace");
+    private void loadWorkspace(
+            List<ShortcutInfo> allDeepShortcuts, LoaderMemoryLogger memoryLogger) {
+        loadWorkspace(allDeepShortcuts, Favorites.CONTENT_URI,
+                null /* selection */, memoryLogger);
+    }
 
+    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");
         }
     }
 
-    private void loadWorkspaceImpl(
+    protected void loadWorkspace(
             List<ShortcutInfo> allDeepShortcuts,
+            Uri contentUri,
             String selection,
             @Nullable LoaderMemoryLogger memoryLogger) {
         final Context context = mApp.getContext();
@@ -363,7 +377,7 @@
         final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
 
         boolean clearDb = false;
-        if (!mApp.getModel().getModelDbController().migrateGridIfNeeded()) {
+        if (!GridSizeMigrationUtil.migrateGridIfNeeded(context)) {
             // Migration failed. Clear workspace.
             clearDb = true;
         }
@@ -388,9 +402,8 @@
             mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
 
             mShortcutKeyToPinnedShortcuts = new HashMap<>();
-            ModelDbController dbController = mApp.getModel().getModelDbController();
             final LoaderCursor c = new LoaderCursor(
-                    dbController.query(TABLE_NAME, null, selection, null, null),
+                    contentResolver.query(contentUri, null, selection, null, null), contentUri,
                     mApp, mUserManagerState);
             final Bundle extras = c.getExtras();
             mDbName = extras == null ? null : extras.getString(Settings.EXTRA_DB_NAME);
@@ -1099,9 +1112,12 @@
         FileLog.d(TAG, widgetDimension.toString());
     }
 
-    private static void logASplit(String label) {
-        if (DEBUG) {
-            Log.d(TAG, label);
+    private static void logASplit(@Nullable TimingLogger timingLogger, String label) {
+        if (timingLogger != null) {
+            timingLogger.addSplit(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 43d8958..97bce8c 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -19,10 +19,11 @@
 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;
@@ -30,6 +31,7 @@
 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;
@@ -41,7 +43,6 @@
 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;
@@ -53,22 +54,18 @@
 
 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;
 
@@ -76,6 +73,7 @@
 
 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
@@ -84,8 +82,6 @@
 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;
@@ -96,36 +92,26 @@
 
     private synchronized void createDbIfNotExists() {
         if (mOpenHelper == null) {
-            mOpenHelper = createDatabaseHelper(false /* forMigration */);
-            RestoreDbTask.restoreIfNeeded(mContext, this);
+            mOpenHelper = DatabaseHelper.createDatabaseHelper(
+                    mContext, false /* forMigration */);
+
+            RestoreDbTask.restoreIfNeeded(mContext, mOpenHelper);
         }
     }
 
-    protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
-        boolean isSandbox = mContext instanceof SandboxContext;
-        String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
-
-        // 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 */);
+    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;
         }
-        databaseHelper.mHotseatRestoreTableExists = tableExists(
-                databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
 
-        databaseHelper.initIds();
-        return databaseHelper;
+        final DatabaseHelper helper = src.get();
+        mOpenHelper = dst.get();
+        copyTable(helper.getReadableDatabase(), Favorites.TABLE_NAME,
+                mOpenHelper.getWritableDatabase(), targetTableName, mContext);
+        helper.close();
+        return true;
     }
 
     /**
@@ -281,40 +267,42 @@
     }
 
     /**
-     * Migrates the DB if needed, and returns false if the migration failed
-     * and DB needs to be cleared.
-     * @return true if migration was success or ignored, false if migration failed
-     * and the DB should be reset.
+     * Updates the current DB and copies all the existing data to the temp table
+     * @param dbFile name of the target db file name
      */
-    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())) {
-            Log.e(TAG, "migrateGridIfNeeded - target db is same as current: " + targetDbName);
-            return false;
-        }
-        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();
-            }
-        }
+    @WorkerThread
+    public boolean updateCurrentOpenHelper(String dbFile) {
+        createDbIfNotExists();
+        return prepForMigration(
+                dbFile,
+                Favorites.TMP_TABLE,
+                () -> mOpenHelper,
+                () -> DatabaseHelper.createDatabaseHelper(
+                        mContext, true /* forMigration */));
     }
 
     /**
-     * Returns the underlying model database
+     * Returns the current DatabaseHelper.
+     * Only for tests
      */
-    public SQLiteDatabase getDb() {
+    @WorkerThread
+    public DatabaseHelper getDatabaseHelper() {
         createDbIfNotExists();
-        return mOpenHelper.getWritableDatabase();
+        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);
     }
 
     private void onAddOrDeleteOp(SQLiteDatabase db) {
@@ -357,7 +345,8 @@
     }
 
     private void clearFlagEmptyDbCreated() {
-        LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()));
+        LauncherPrefs.getPrefs(mContext).edit()
+                .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
     }
 
     /**
@@ -370,8 +359,9 @@
     @WorkerThread
     public synchronized void loadDefaultFavoritesIfNecessary() {
         createDbIfNotExists();
+        SharedPreferences sp = LauncherPrefs.getPrefs(mContext);
 
-        if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()))) {
+        if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
             Log.d(TAG, "loading default workspace");
 
             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
@@ -489,27 +479,4 @@
         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/model/WidgetItem.java b/src/com/android/launcher3/model/WidgetItem.java
index 7198d54..c99b889 100644
--- a/src/com/android/launcher3/model/WidgetItem.java
+++ b/src/com/android/launcher3/model/WidgetItem.java
@@ -3,6 +3,7 @@
 import static com.android.launcher3.Utilities.ATLEAST_S;
 
 import android.annotation.SuppressLint;
+import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
@@ -25,13 +26,15 @@
     public final ShortcutConfigActivityInfo activityInfo;
 
     public final String label;
+    public final CharSequence description;
     public final int spanX, spanY;
 
     public WidgetItem(LauncherAppWidgetProviderInfo info,
-            InvariantDeviceProfile idp, IconCache iconCache) {
+            InvariantDeviceProfile idp, IconCache iconCache, Context context) {
         super(info.provider, info.getProfile());
 
         label = iconCache.getTitleNoCache(info);
+        description = ATLEAST_S ? info.loadDescription(context) : null;
         widgetInfo = info;
         activityInfo = null;
 
@@ -43,6 +46,7 @@
         super(info.getComponent(), info.getUser());
         label = info.isPersistable() ? iconCache.getTitleNoCache(info) :
                 Utilities.trim(info.getLabel(pm));
+        description = null;
         widgetInfo = null;
         activityInfo = info;
         spanX = spanY = 1;
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index c718dcc..48969fc 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, LauncherAppState.getInstance(context), ums);
+        LoaderCursor lc = new LoaderCursor(c, null, 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 a6e064a..ac72164 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -16,8 +16,6 @@
 
 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;
@@ -49,8 +47,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;
@@ -85,12 +83,12 @@
     /**
      * Tries to restore the backup DB if needed
      */
-    public static void restoreIfNeeded(Context context, ModelDbController dbController) {
+    public static void restoreIfNeeded(Context context, DatabaseHelper helper) {
         if (!isPending(context)) {
             return;
         }
-        if (!performRestore(context, dbController)) {
-            dbController.createEmptyDB();
+        if (!performRestore(context, helper)) {
+            helper.createEmptyDB(helper.getWritableDatabase());
         }
 
         // Obtain InvariantDeviceProfile first before setting pending to false, so
@@ -104,12 +102,12 @@
         idp.reinitializeAfterRestore(context);
     }
 
-    private static boolean performRestore(Context context, ModelDbController controller) {
-        SQLiteDatabase db = controller.getDb();
+    private static boolean performRestore(Context context, DatabaseHelper helper) {
+        SQLiteDatabase db = helper.getWritableDatabase();
         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
             RestoreDbTask task = new RestoreDbTask();
-            task.sanitizeDB(context, controller, db, new BackupManager(context));
-            task.restoreAppWidgetIdsIfExists(context, controller);
+            task.sanitizeDB(context, helper, db, new BackupManager(context));
+            task.restoreAppWidgetIdsIfExists(context, helper);
             t.commit();
             return true;
         } catch (Exception e) {
@@ -131,10 +129,10 @@
      * @return number of items deleted.
      */
     @VisibleForTesting
-    protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
+    protected int sanitizeDB(Context context, DatabaseHelper helper, SQLiteDatabase db,
             BackupManager backupManager) throws Exception {
         // Primary user ids
-        long myProfileId = controller.getSerialNumberForUser(myUserHandle());
+        long myProfileId = helper.getDefaultUserSerial();
         long oldProfileId = getDefaultProfileId(db);
         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
@@ -146,7 +144,7 @@
             long oldManagedProfileId = oldManagedProfileIds.keyAt(i);
             UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId);
             if (user != null) {
-                long newManagedProfileId = controller.getSerialNumberForUser(user);
+                long newManagedProfileId = helper.getSerialNumberForUser(user);
                 profileMapping.put(oldManagedProfileId, newManagedProfileId);
             }
         }
@@ -215,7 +213,7 @@
         }
 
         // Override shortcuts
-        maybeOverrideShortcuts(context, controller, db, myProfileId);
+        maybeOverrideShortcuts(context, helper, db, myProfileId);
 
         return itemsDeleted;
     }
@@ -323,11 +321,11 @@
                 .putSync(RESTORE_DEVICE.to(new DeviceGridState(context).getDeviceType()));
     }
 
-    private void restoreAppWidgetIdsIfExists(Context context, ModelDbController controller) {
+    private void restoreAppWidgetIdsIfExists(Context context, DatabaseHelper helper) {
         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, controller,
+            AppWidgetsRestoredReceiver.restoreAppWidgetIds(context, helper,
                     IntArray.fromConcatString(lp.get(OLD_APP_WIDGET_IDS)).toArray(),
                     IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
                     host);
@@ -345,7 +343,7 @@
                 APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
     }
 
-    protected static void maybeOverrideShortcuts(Context context, ModelDbController controller,
+    protected static void maybeOverrideShortcuts(Context context, DatabaseHelper helper,
             SQLiteDatabase db, long currentUser) {
         Map<String, LauncherActivityInfo> activityOverrides = ApiWrapper.getActivityOverrides(
                 context);
@@ -369,7 +367,7 @@
                 if (override != null) {
                     ContentValues values = new ContentValues();
                     values.put(Favorites.PROFILE_ID,
-                            controller.getSerialNumberForUser(override.getUser()));
+                            helper.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 7c5ef4d..e509235 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.ModelDbController;
+import com.android.launcher3.model.DatabaseHelper;
 import com.android.launcher3.pm.UserCache;
 
 /**
@@ -106,7 +106,7 @@
 
     public int commit() {
         if (mCommitParams != null) {
-            mCommitParams.mDbController.update(
+            mCommitParams.mDatabaseHelper.getWritableDatabase().update(
                     Favorites.TABLE_NAME, getValues(mContext),
                     mCommitParams.mWhere, mCommitParams.mSelectionArgs);
         }
@@ -115,12 +115,12 @@
 
     public static final class CommitParams {
 
-        final ModelDbController mDbController;
+        final DatabaseHelper mDatabaseHelper;
         final String mWhere;
         final String[] mSelectionArgs;
 
-        public CommitParams(ModelDbController controller, String where, String[] selectionArgs) {
-            mDbController = controller;
+        public CommitParams(DatabaseHelper helper, String where, String[] selectionArgs) {
+            mDatabaseHelper = helper;
             mWhere = where;
             mSelectionArgs = selectionArgs;
         }
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 67d21f7..f269434 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -157,8 +157,15 @@
                 previewBounds.right -= padding;
             }
             if (mAppWidgetHostViewPreview != null) {
-                previewWidth = mAppWidgetHostViewPreview.getMeasuredWidth();
-                previewHeight = mAppWidgetHostViewPreview.getMeasuredHeight();
+                float previewScale = mAppWidgetHostViewPreview.getScaleX();
+                int widgetWidth = mAppWidgetHostViewPreview.getMeasuredWidth();
+                int widgetHeight = mAppWidgetHostViewPreview.getMeasuredHeight();
+                previewWidth = Math.round(widgetWidth * previewScale);
+                previewHeight = Math.round(widgetHeight * previewScale);
+
+                previewBounds.offset(
+                        Math.round(widgetWidth * (previewScale - 1) / 2),
+                        Math.round(widgetHeight * (previewScale - 1) / 2));
             } else {
                 previewWidth = preview.getIntrinsicWidth();
                 previewHeight = preview.getIntrinsicHeight();
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 80bc1a7..c30342a 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,18 +16,15 @@
 
 package com.android.launcher3.widget;
 
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
-import static com.android.launcher3.Utilities.ATLEAST_S;
-import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_WIDGET_CENTERING;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
+import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
 import android.os.Process;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Size;
@@ -47,7 +44,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.CheckLongPressHelper;
-import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.icons.FastBitmapDrawable;
@@ -55,7 +51,6 @@
 import com.android.launcher3.icons.cache.HandlerRunnable;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.util.WidgetSizes;
 
 import java.util.function.Consumer;
 
@@ -75,40 +70,8 @@
 
     private static final int FADE_IN_DURATION_MS = 90;
 
-    /** Widget cell width is calculated by multiplying this factor to grid cell width. */
-    private static final float WIDTH_SCALE = 3f;
-
-    /** Widget preview width is calculated by multiplying this factor to the widget cell width. */
-    private static final float PREVIEW_SCALE = 0.8f;
-
     /**
-     * The maximum dimension that can be used as the size in
-     * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int)}.
-     *
-     * <p>This is equal to (1 << MeasureSpec.MODE_SHIFT) - 1.
-     */
-    private static final int MAX_MEASURE_SPEC_DIMENSION = (1 << 30) - 1;
-
-    /**
-     * The target preview width, in pixels, of a widget or a shortcut.
-     *
-     * <p>The actual preview width may be smaller than or equal to this value subjected to scaling.
-     */
-    protected int mTargetPreviewWidth;
-
-    /**
-     * The target preview height, in pixels, of a widget or a shortcut.
-     *
-     * <p>The actual preview height may be smaller than or equal to this value subjected to scaling.
-     */
-    protected int mTargetPreviewHeight;
-
-    protected int mPresetPreviewSize;
-
-    private int mCellSize;
-
-    /**
-     * The scale of the preview container.
+     * The requested scale of the preview container. It can be lower than this as well.
      */
     private float mPreviewContainerScale = 1f;
 
@@ -119,7 +82,8 @@
     private TextView mWidgetDims;
     private TextView mWidgetDescription;
 
-    protected WidgetItem mItem;
+    private WidgetItem mItem;
+    private Size mWidgetSize;
 
     private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
 
@@ -150,18 +114,11 @@
         mWidgetPreviewLoader = new DatabaseWidgetPreviewLoader(context);
         mLongPressHelper = new CheckLongPressHelper(this);
         mLongPressHelper.setLongPressTimeoutFactor(1);
+        mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
+        mWidgetSize = new Size(0, 0);
 
-        setContainerWidth();
-        setWillNotDraw(false);
         setClipToPadding(false);
         setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
-        mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
-    }
-
-    private void setContainerWidth() {
-        mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE);
-        mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
-        mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
     }
 
     @Override
@@ -206,7 +163,6 @@
         mWidgetDims.setText(null);
         mWidgetDescription.setText(null);
         mWidgetDescription.setVisibility(GONE);
-        mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
 
         if (mActiveRequest != null) {
             mActiveRequest.cancel();
@@ -218,7 +174,9 @@
         }
         mAppWidgetHostViewPreview = null;
         mAppWidgetHostViewScale = 1f;
+        mPreviewContainerScale = 1f;
         mItem = null;
+        mWidgetSize = new Size(0, 0);
     }
 
     public void setSourceContainer(int sourceContainer) {
@@ -248,17 +206,12 @@
      */
     public void applyFromCellItem(WidgetItem item, float previewScale,
             @NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) {
-        // setPreviewSize
-        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
-        Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, item);
-        mTargetPreviewWidth = widgetSize.getWidth();
-        mTargetPreviewHeight = widgetSize.getHeight();
         mPreviewContainerScale = previewScale;
 
-        applyPreviewOnAppWidgetHostView(item);
-
         Context context = getContext();
         mItem = item;
+        mWidgetSize = getWidgetItemSizePx(getContext(), mActivity.getDeviceProfile(), mItem);
+
         mWidgetName.setText(mItem.label);
         mWidgetName.setContentDescription(
                 context.getString(R.string.widget_preview_context_description, mItem.label));
@@ -266,14 +219,11 @@
                 mItem.spanX, mItem.spanY));
         mWidgetDims.setContentDescription(context.getString(
                 R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
-        if (ATLEAST_S && mItem.widgetInfo != null) {
-            CharSequence description = mItem.widgetInfo.loadDescription(context);
-            if (description != null && description.length() > 0) {
-                mWidgetDescription.setText(description);
-                mWidgetDescription.setVisibility(VISIBLE);
-            } else {
-                mWidgetDescription.setVisibility(GONE);
-            }
+        if (!TextUtils.isEmpty(mItem.description)) {
+            mWidgetDescription.setText(mItem.description);
+            mWidgetDescription.setVisibility(VISIBLE);
+        } else {
+            mWidgetDescription.setVisibility(GONE);
         }
 
         if (item.activityInfo != null) {
@@ -282,68 +232,31 @@
             setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer));
         }
 
-        ensurePreviewWithCallback(callback, cachedPreview);
-    }
-
-    private static class ScaledAppWidgetHostView extends LauncherAppWidgetHostView {
-        private boolean mKeepOrigForDragging = true;
-
-        ScaledAppWidgetHostView(Context context) {
-            super(context);
-        }
-
-        /**
-         * Set if the view will keep its original scale when dragged
-         * @param isKeepOrig True if keep original scale when dragged, false otherwise
-         */
-        public void setKeepOrigForDragging(boolean isKeepOrig) {
-            mKeepOrigForDragging = isKeepOrig;
-        }
-
-        /**
-         * @return True if the view is set to preserve original scale when dragged, false otherwise
-         */
-        public boolean isKeepOrigForDragging() {
-            return mKeepOrigForDragging;
-        }
-
-        @Override
-        public void startDrag() {
-            super.startDrag();
-            if (!isKeepOrigForDragging()) {
-                // restore to original scale when being dragged, if set to do so
-                setScaleToFit(1.0f);
-            }
-            // When the drag start, translations need to be set to zero to center the view
-            getTranslateDelegate().setTranslation(INDEX_WIDGET_CENTERING, 0f, 0f);
-        }
-    }
-
-    private void applyPreviewOnAppWidgetHostView(WidgetItem item) {
         if (mRemoteViewsPreview != null) {
-            mAppWidgetHostViewPreview = createAppWidgetHostView(getContext());
+            mAppWidgetHostViewPreview = createAppWidgetHostView(context);
             setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo,
                     mRemoteViewsPreview);
-            return;
+        } else if (item.hasPreviewLayout()) {
+            // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview
+            // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView,
+            // which supports applying local color extraction during drag & drop.
+            mAppWidgetHostViewPreview = isLauncherContext(context)
+                    ? new LauncherAppWidgetHostView(context)
+                    : createAppWidgetHostView(context);
+            LauncherAppWidgetProviderInfo providerInfo =
+                    fromProviderInfo(context, item.widgetInfo.clone());
+            // A hack to force the initial layout to be the preview layout since there is no API for
+            // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+            // proper solution is to use RemoteViews(PackageName, LayoutId).
+            providerInfo.initialLayout = item.widgetInfo.previewLayout;
+            setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, providerInfo, null);
+        } else if (cachedPreview != null) {
+            applyPreview(cachedPreview);
+        } else {
+            if (mActiveRequest == null) {
+                mActiveRequest = mWidgetPreviewLoader.loadPreview(mItem, mWidgetSize, callback);
+            }
         }
-
-        if (!item.hasPreviewLayout()) return;
-
-        Context context = getContext();
-        // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview as
-        // a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, which
-        // supports applying local color extraction during drag & drop.
-        mAppWidgetHostViewPreview = isLauncherContext(context)
-                ? new ScaledAppWidgetHostView(context)
-                : createAppWidgetHostView(context);
-        LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
-                LauncherAppWidgetProviderInfo.fromProviderInfo(context, item.widgetInfo.clone());
-        // A hack to force the initial layout to be the preview layout since there is no API for
-        // rendering a preview layout for work profile apps yet. For non-work profile layout, a
-        // proper solution is to use RemoteViews(PackageName, LayoutId).
-        launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
-        setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
-                launcherAppWidgetProviderInfo, /* remoteViews= */ null);
     }
 
     private void setAppWidgetHostViewPreview(
@@ -353,6 +266,43 @@
         appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
         appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo);
         appWidgetHostViewPreview.updateAppWidget(remoteViews);
+        appWidgetHostViewPreview.setClipToPadding(false);
+        appWidgetHostViewPreview.setClipChildren(false);
+
+        FrameLayout.LayoutParams widgetHostLP = new FrameLayout.LayoutParams(
+                mWidgetSize.getWidth(), mWidgetSize.getHeight(), Gravity.CENTER);
+        mWidgetImageContainer.addView(appWidgetHostViewPreview, /* index= */ 0, widgetHostLP);
+        mWidgetImage.setVisibility(View.GONE);
+        applyPreview(null);
+
+        appWidgetHostViewPreview.addOnLayoutChangeListener(
+                (v, l, t, r, b, ol, ot, or, ob) ->
+                        updateAppWidgetHostScale(appWidgetHostViewPreview));
+    }
+
+    private void updateAppWidgetHostScale(NavigableAppWidgetHostView view) {
+        // Scale the content such that all of the content is visible
+        int contentWidth = view.getWidth();
+        int contentHeight = view.getHeight();
+
+        if (view.getChildCount() == 1) {
+            View content = view.getChildAt(0);
+            // Take the content width based on the edge furthest from the center, so that when
+            // scaling the hostView, the farthest edge is still visible.
+            contentWidth = 2 * Math.max(contentWidth / 2 - content.getLeft(),
+                    content.getRight() - contentWidth / 2);
+            contentHeight = 2 * Math.max(contentHeight / 2 - content.getTop(),
+                    content.getBottom() - contentHeight / 2);
+        }
+
+        if (contentWidth <= 0 || contentHeight <= 0) {
+            mAppWidgetHostViewScale = 1;
+        } else {
+            float pWidth = mWidgetImageContainer.getWidth();
+            float pHeight = mWidgetImageContainer.getHeight();
+            mAppWidgetHostViewScale = Math.min(pWidth / contentWidth, pHeight / contentHeight);
+        }
+        view.setScaleToFit(mAppWidgetHostViewScale);
     }
 
     public WidgetImageView getWidgetView() {
@@ -372,17 +322,6 @@
         if (bitmap != null) {
             Drawable drawable = new RoundDrawableWrapper(
                     new FastBitmapDrawable(bitmap), mEnforcedCornerRadius);
-
-            // Scale down the preview size if it's wider than the cell.
-            float scale = 1f;
-            if (mTargetPreviewWidth > 0) {
-                float maxWidth = mTargetPreviewWidth;
-                float previewWidth = drawable.getIntrinsicWidth() * mPreviewContainerScale;
-                scale = Math.min(maxWidth / previewWidth, 1);
-            }
-            setContainerSize(
-                    Math.round(drawable.getIntrinsicWidth() * scale * mPreviewContainerScale),
-                    Math.round(drawable.getIntrinsicHeight() * scale * mPreviewContainerScale));
             mWidgetImage.setDrawable(drawable);
             mWidgetImage.setVisibility(View.VISIBLE);
             if (mAppWidgetHostViewPreview != null) {
@@ -415,76 +354,6 @@
         }
     }
 
-    private void setContainerSize(int width, int height) {
-        LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams();
-        layoutParams.width = width;
-        layoutParams.height = height;
-        mWidgetImageContainer.setLayoutParams(layoutParams);
-    }
-
-    /**
-     * Ensures that the preview is already loaded or being loaded. If the preview is not loaded,
-     * it applies the provided cachedPreview. If that is null, it starts a loader and notifies the
-     * callback on successful load.
-     */
-    private void ensurePreviewWithCallback(Consumer<Bitmap> callback,
-            @Nullable Bitmap cachedPreview) {
-        if (mAppWidgetHostViewPreview != null) {
-            int containerWidth = (int) (mTargetPreviewWidth * mPreviewContainerScale);
-            int containerHeight = (int) (mTargetPreviewHeight * mPreviewContainerScale);
-            setContainerSize(containerWidth, containerHeight);
-            boolean shouldMeasureAndScale = false;
-            if (mAppWidgetHostViewPreview.getChildCount() == 1) {
-                View widgetContent = mAppWidgetHostViewPreview.getChildAt(0);
-                ViewGroup.LayoutParams layoutParams = widgetContent.getLayoutParams();
-                // We only scale preview if both the width & height of the outermost view group are
-                // not set to MATCH_PARENT.
-                shouldMeasureAndScale =
-                        layoutParams.width != MATCH_PARENT && layoutParams.height != MATCH_PARENT;
-                if (shouldMeasureAndScale) {
-                    setNoClip(mWidgetImageContainer);
-                    setNoClip(mAppWidgetHostViewPreview);
-                    mAppWidgetHostViewScale = measureAndComputeWidgetPreviewScale();
-                }
-            }
-
-            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
-                    mTargetPreviewWidth, mTargetPreviewHeight, Gravity.FILL);
-            mAppWidgetHostViewPreview.setLayoutParams(params);
-
-            if (!shouldMeasureAndScale
-                    && mAppWidgetHostViewPreview instanceof ScaledAppWidgetHostView) {
-                // If the view is not measured & scaled, at least one side will match the grid size,
-                // so it should be safe to restore the original scale once it is dragged.
-                ScaledAppWidgetHostView tempView =
-                        (ScaledAppWidgetHostView) mAppWidgetHostViewPreview;
-                tempView.setKeepOrigForDragging(false);
-                tempView.setScaleToFit(mPreviewContainerScale);
-            } else if (!shouldMeasureAndScale) {
-                mAppWidgetHostViewPreview.setScaleToFit(mPreviewContainerScale);
-            } else {
-                mAppWidgetHostViewPreview.setScaleToFit(mAppWidgetHostViewScale);
-            }
-            mAppWidgetHostViewPreview.getTranslateDelegate().setTranslation(
-                    INDEX_WIDGET_CENTERING,
-                    -(params.width - (params.width * mPreviewContainerScale)) / 2.0f,
-                    -(params.height - (params.height * mPreviewContainerScale)) / 2.0f);
-            mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
-            mWidgetImage.setVisibility(View.GONE);
-            applyPreview(null);
-            return;
-        }
-        if (cachedPreview != null) {
-            applyPreview(cachedPreview);
-            return;
-        }
-        if (mActiveRequest != null) {
-            return;
-        }
-        mActiveRequest = mWidgetPreviewLoader.loadPreview(
-                mItem, new Size(mTargetPreviewWidth, mTargetPreviewHeight), callback);
-    }
-
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         super.onTouchEvent(ev);
@@ -522,61 +391,20 @@
         info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
     }
 
-    private static void setNoClip(ViewGroup view) {
-        view.setClipChildren(false);
-        view.setClipToPadding(false);
-    }
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
 
-    private float measureAndComputeWidgetPreviewScale() {
-        if (mAppWidgetHostViewPreview.getChildCount() != 1) {
-            return 1f;
+        mAppWidgetHostViewScale = mPreviewContainerScale;
+        int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
+        containerLp.width = Math.round(mWidgetSize.getWidth() * mAppWidgetHostViewScale);
+        if (containerLp.width > maxWidth) {
+            containerLp.width = maxWidth;
+            mAppWidgetHostViewScale = (float) containerLp.width / mWidgetSize.getWidth();
         }
+        containerLp.height = Math.round(mWidgetSize.getHeight() * mAppWidgetHostViewScale);
+        // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass
 
-        // Measure the largest possible width & height that the app widget wants to display.
-        mAppWidgetHostViewPreview.measure(
-                makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED),
-                makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED));
-        if (mRemoteViewsPreview != null) {
-            // If RemoteViews contains multiple sizes, the best fit sized RemoteViews will be
-            // selected in onLayout. To work out the right measurement, let's layout and then
-            // measure again.
-            mAppWidgetHostViewPreview.layout(
-                    /* left= */ 0,
-                    /* top= */ 0,
-                    /* right= */ mTargetPreviewWidth,
-                    /* bottom= */ mTargetPreviewHeight);
-            mAppWidgetHostViewPreview.measure(
-                    makeMeasureSpec(mTargetPreviewWidth, MeasureSpec.UNSPECIFIED),
-                    makeMeasureSpec(mTargetPreviewHeight, MeasureSpec.UNSPECIFIED));
-
-        }
-        View widgetContent = mAppWidgetHostViewPreview.getChildAt(0);
-        int appWidgetContentWidth = widgetContent.getMeasuredWidth();
-        int appWidgetContentHeight = widgetContent.getMeasuredHeight();
-        if (appWidgetContentWidth == 0 || appWidgetContentHeight == 0) {
-            return 1f;
-        }
-
-        // If the width / height of the widget content is set to wrap content, overrides the width /
-        // height with the measured dimension. This avoids incorrect measurement after scaling.
-        FrameLayout.LayoutParams layoutParam =
-                (FrameLayout.LayoutParams) widgetContent.getLayoutParams();
-        if (layoutParam.width == WRAP_CONTENT) {
-            layoutParam.width = widgetContent.getMeasuredWidth();
-        }
-        if (layoutParam.height == WRAP_CONTENT) {
-            layoutParam.height = widgetContent.getMeasuredHeight();
-        }
-        widgetContent.setLayoutParams(layoutParam);
-
-        int horizontalPadding = mAppWidgetHostViewPreview.getPaddingStart()
-                + mAppWidgetHostViewPreview.getPaddingEnd();
-        int verticalPadding = mAppWidgetHostViewPreview.getPaddingTop()
-                + mAppWidgetHostViewPreview.getPaddingBottom();
-        return Math.min(
-                (mTargetPreviewWidth - horizontalPadding) * mPreviewContainerScale
-                        / appWidgetContentWidth,
-                (mTargetPreviewHeight - verticalPadding) * mPreviewContainerScale
-                        / appWidgetContentHeight);
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 23cdae9..846dafd 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -252,8 +252,11 @@
         super.setInsets(insets);
         int bottomPadding = Math.max(insets.bottom, mNavBarScrimHeight);
 
-        mContent.setPadding(mContent.getPaddingStart(),
-                mContent.getPaddingTop(), mContent.getPaddingEnd(),
+        View widgetsTable = findViewById(R.id.widgets_table);
+        widgetsTable.setPadding(
+                widgetsTable.getPaddingLeft(),
+                widgetsTable.getPaddingTop(),
+                widgetsTable.getPaddingRight(),
                 bottomPadding);
         if (bottomPadding > 0) {
             setupNavBarColor();
diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
index 1b743e8..2f16065 100644
--- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
+++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java
@@ -129,7 +129,7 @@
                         LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
 
                 widgetsAndShortcuts.add(new WidgetItem(
-                        launcherWidgetInfo, idp, app.getIconCache()));
+                        launcherWidgetInfo, idp, app.getIconCache(), app.getContext()));
                 updatedItems.add(launcherWidgetInfo);
             }
 
@@ -200,7 +200,8 @@
                                     app.getContext().getPackageManager()));
                         } else {
                             items.set(i, new WidgetItem(item.widgetInfo,
-                                    app.getInvariantDeviceProfile(), app.getIconCache()));
+                                    app.getInvariantDeviceProfile(), app.getIconCache(),
+                                    app.getContext()));
                         }
                     }
                 }
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index b472cdb..601b07e 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -90,6 +90,7 @@
     public static final String REQUEST_DISABLE_TRANSIENT_TASKBAR = "disable-transient-taskbar";
     public static final String REQUEST_UNSTASH_TASKBAR_IF_STASHED = "unstash-taskbar-if-stashed";
     public static final String REQUEST_STASHED_TASKBAR_HEIGHT = "stashed-taskbar-height";
+    public static final String REQUEST_STASHED_TASKBAR_SCALE = "taskbar-stash-handle-scale";
     public static final String REQUEST_RECREATE_TASKBAR = "recreate-taskbar";
     public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
     public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
diff --git a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
index cea95e5..0a1a9ba 100644
--- a/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
+++ b/tests/src/com/android/launcher3/model/DbDowngradeHelperTest.java
@@ -39,7 +39,6 @@
 
 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;
@@ -223,9 +222,7 @@
     private class MyDatabaseHelper extends DatabaseHelper {
 
         MyDatabaseHelper() {
-            super(mContext, DB_FILE,
-                    UserCache.INSTANCE.get(mContext)::getSerialNumberForUser,
-                    () -> { });
+            super(mContext, DB_FILE, false);
         }
 
         @Override
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index 63dbaa7..f24f0da 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.model
 
-import android.content.ContentValues
 import android.content.Context
 import android.content.Intent
 import android.database.Cursor
@@ -24,7 +23,6 @@
 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
@@ -33,7 +31,10 @@
 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
@@ -42,12 +43,11 @@
 @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,17 +61,13 @@
 
     @Before
     fun setUp() {
-        context = InstrumentationRegistry.getInstrumentation().targetContext
-        dbHelper =
-            DatabaseHelper(
-                context,
-                null,
-                UserCache.INSTANCE.get(context)::getSerialNumberForUser
-            ) {}
-        db = dbHelper.writableDatabase
+        modelHelper = LauncherModelHelper()
+        context = modelHelper.sandboxContext
+        db = modelHelper.provider.db
 
         validPackages =
             setOf(
+                TEST_PACKAGE,
                 testPackage1,
                 testPackage2,
                 testPackage3,
@@ -90,6 +86,11 @@
         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
@@ -98,26 +99,26 @@
     @Throws(Exception::class)
     fun testMigration() {
         // Src Hotseat icons
-        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)
+        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)
         // Src grid icons
         // _ _ _ _ _
         // _ _ _ _ 5
         // _ _ 6 _ 7
         // _ _ 8 _ 9
         // _ _ _ _ _
-        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)
+        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)
 
         // Dest hotseat icons
-        addItem(ITEM_TYPE_SHORTCUT, 1, CONTAINER_HOTSEAT, 0, 0, testPackage2)
+        modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2)
         // Dest grid icons
-        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage10)
+        modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage10)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -125,7 +126,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -136,13 +138,12 @@
 
         // Check hotseat items
         var c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -167,13 +168,12 @@
 
         // Check workspace items
         c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 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
-        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)
+        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)
         // Workspace items in grid A
         // _ _ _ _ _
         // _ _ _ _ 5
         // _ _ 6 _ 7
         // _ _ 8 _ _
         // _ _ _ _ _
-        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)
+        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)
 
         // Hotseat items in grid B
         // 2 _ _ _
-        addItem(ITEM_TYPE_SHORTCUT, 0, CONTAINER_HOTSEAT, 0, 0, testPackage2)
+        modelHelper.addItem(SHORTCUT, 0, HOTSEAT, 0, 0, testPackage2)
         // Workspace items in grid B
         // _ _ _ _
         // _ _ _ 10
         // _ _ _ _
         // _ _ _ _
-        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 1, 3, testPackage10)
+        modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 3, testPackage10)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -241,7 +241,8 @@
         val readerGridB = DbReader(db, TABLE_NAME, context, validPackages)
         // migrate from A -> B
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
@@ -252,13 +253,12 @@
 
         // Check hotseat items in grid B
         var c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -272,13 +272,12 @@
 
         // Check workspace items in grid B
         c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -295,11 +294,12 @@
         assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))
 
         // add item in B
-        addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9)
+        modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 2, testPackage9)
 
         // migrate from B -> A
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             readerGridB,
             readerGridA,
             5,
@@ -309,13 +309,12 @@
         )
         // Check hotseat items in grid A
         c =
-            db.query(
-                TMP_TABLE,
+            context.contentResolver.query(
+                TMP_CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -329,13 +328,12 @@
 
         // Check workspace items in grid A
         c =
-            db.query(
-                TMP_TABLE,
+            context.contentResolver.query(
+                TMP_CONTENT_URI,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -356,11 +354,12 @@
         assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
 
         // remove item from B
-        db.delete(TMP_TABLE, "$_ID=7", null)
+        modelHelper.deleteItem(7, TMP_TABLE)
 
         // migrate from A -> B
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             readerGridA,
             readerGridB,
             idp.numDatabaseHotseatIcons,
@@ -371,13 +370,12 @@
 
         // Check hotseat items in grid B
         c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -391,13 +389,12 @@
 
         // Check workspace items in grid B
         c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, CELLX, CELLY, INTENT),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -446,28 +443,10 @@
     fun migrateToLargerHotseat() {
         val srcHotseatItems =
             intArrayOf(
-                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)
+                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)
             )
         val numSrcDatabaseHotseatIcons = srcHotseatItems.size
         idp.numDatabaseHotseatIcons = 6
@@ -476,7 +455,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -487,13 +467,12 @@
 
         // Check hotseat items
         val c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -522,11 +501,11 @@
 
     @Test
     fun migrateFromLargerHotseat() {
-        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)
+        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)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -534,7 +513,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -545,13 +525,12 @@
 
         // Check hotseat items
         val c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(SCREEN, INTENT),
                 "container=$CONTAINER_HOTSEAT",
                 null,
                 SCREEN,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -589,11 +568,11 @@
         enableNewMigrationLogic("4,4")
 
         // Setup src grid
-        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)
+        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)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 6
@@ -602,7 +581,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -613,13 +593,12 @@
 
         // Get workspace items
         val c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -651,11 +630,11 @@
         enableNewMigrationLogic("2,2")
 
         // Setup src grid
-        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)
+        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)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 5
@@ -663,7 +642,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -674,13 +654,12 @@
 
         // Get workspace items
         val c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -712,11 +691,11 @@
         enableNewMigrationLogic("5,5")
 
         // Setup src grid
-        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)
+        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)
 
         idp.numDatabaseHotseatIcons = 4
         idp.numColumns = 4
@@ -724,7 +703,8 @@
         val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
         val destReader = DbReader(db, TABLE_NAME, context, validPackages)
         GridSizeMigrationUtil.migrate(
-            dbHelper,
+            context,
+            db,
             srcReader,
             destReader,
             idp.numDatabaseHotseatIcons,
@@ -735,13 +715,12 @@
 
         // Get workspace items
         val c =
-            db.query(
-                TABLE_NAME,
+            context.contentResolver.query(
+                CONTENT_URI,
                 arrayOf(INTENT, SCREEN),
                 "container=$CONTAINER_DESKTOP",
                 null,
                 null,
-                null,
                 null
             )
                 ?: throw IllegalStateException()
@@ -768,48 +747,4 @@
     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 78812c0..d192be4 100644
--- a/tests/src/com/android/launcher3/model/LoaderCursorTest.java
+++ b/tests/src/com/android/launcher3/model/LoaderCursorTest.java
@@ -59,6 +59,7 @@
 
 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;
@@ -101,7 +102,7 @@
         });
 
         UserManagerState ums = new UserManagerState();
-        mLoaderCursor = new LoaderCursor(mCursor, mApp, ums);
+        mLoaderCursor = new LoaderCursor(mCursor, Favorites.CONTENT_URI, mApp, ums);
         ums.allUsers.put(0, Process.myUserHandle());
     }
 
diff --git a/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java b/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
index 54b8489..2b6f9ff 100644
--- a/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
+++ b/tests/src/com/android/launcher3/provider/LauncherDbUtilsTest.java
@@ -45,7 +45,6 @@
 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;
@@ -166,11 +165,12 @@
     private class MyDatabaseHelper extends DatabaseHelper {
 
         MyDatabaseHelper() {
-            super(mContext, null, UserCache.INSTANCE.get(mContext)::getSerialNumberForUser,
-                    () -> { });
+            super(mContext, null, false);
         }
 
         @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 1b1b294..67de1f5 100644
--- a/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
+++ b/tests/src/com/android/launcher3/provider/RestoreDbTaskTest.java
@@ -46,7 +46,6 @@
 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;
@@ -64,13 +63,13 @@
 
     @Test
     public void testGetProfileId() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(23).getDb();
+        SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
         assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
     }
 
     @Test
     public void testMigrateProfileId() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
@@ -90,7 +89,7 @@
 
     @Test
     public void testChangeDefaultColumn() throws Exception {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
         // Add some mock data
         for (int i = 0; i < 5; i++) {
             ContentValues values = new ContentValues();
@@ -121,20 +120,20 @@
         long workProfileId = myProfileId + 2;
         long workProfileId_old = myProfileId + 3;
 
-        MyModelDbController controller = new MyModelDbController(myProfileId);
-        SQLiteDatabase db = controller.getDb();
+        MyDatabaseHelper helper = new MyDatabaseHelper(myProfileId);
+        SQLiteDatabase db = helper.getWritableDatabase();
         BackupManager bm = spy(new BackupManager(context));
         doReturn(myUserHandle()).when(bm).getUserForAncestralSerialNumber(eq(myProfileId_old));
         doReturn(mWorkUser).when(bm).getUserForAncestralSerialNumber(eq(workProfileId_old));
-        controller.users.put(workProfileId, mWorkUser);
+        helper.users.put(workProfileId, mWorkUser);
 
-        addIconsBulk(controller, 10, 1, myProfileId_old);
-        addIconsBulk(controller, 6, 2, workProfileId_old);
+        addIconsBulk(helper, 10, 1, myProfileId_old);
+        addIconsBulk(helper, 6, 2, workProfileId_old);
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
         RestoreDbTask task = new RestoreDbTask();
-        task.sanitizeDB(context, controller, controller.getDb(), bm);
+        task.sanitizeDB(context, helper, helper.getWritableDatabase(), bm);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -152,20 +151,20 @@
         long myProfileId_old = myProfileId + 1;
         long workProfileId_old = myProfileId + 3;
 
-        MyModelDbController controller = new MyModelDbController(myProfileId);
-        SQLiteDatabase db = controller.getDb();
+        MyDatabaseHelper helper = new MyDatabaseHelper(myProfileId);
+        SQLiteDatabase db = helper.getWritableDatabase();
         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(controller, 10, 1, myProfileId_old);
-        addIconsBulk(controller, 6, 2, workProfileId_old);
+        addIconsBulk(helper, 10, 1, myProfileId_old);
+        addIconsBulk(helper, 6, 2, workProfileId_old);
         assertEquals(10, getItemCountForProfile(db, myProfileId_old));
         assertEquals(6, getItemCountForProfile(db, workProfileId_old));
 
         RestoreDbTask task = new RestoreDbTask();
-        task.sanitizeDB(context, controller, controller.getDb(), bm);
+        task.sanitizeDB(context, helper, helper.getWritableDatabase(), bm);
 
         // All the data has been migrated to the new user ids
         assertEquals(0, getItemCountForProfile(db, myProfileId_old));
@@ -174,13 +173,12 @@
         assertEquals(10, getCount(db, "select * from favorites"));
     }
 
-    private void addIconsBulk(MyModelDbController controller,
-            int count, int screen, long profileId) {
+    private void addIconsBulk(DatabaseHelper helper, 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, controller.generateNewItemId());
+            values.put(LauncherSettings.Favorites._ID, helper.generateNewItemId());
             values.put(LauncherSettings.Favorites.CONTAINER, CONTAINER_DESKTOP);
             values.put(LauncherSettings.Favorites.SCREEN, screen);
             values.put(LauncherSettings.Favorites.CELLX, i % columns);
@@ -191,11 +189,11 @@
             values.put(LauncherSettings.Favorites.ITEM_TYPE, ITEM_TYPE_APPLICATION);
             values.put(LauncherSettings.Favorites.INTENT,
                     new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
-
-            controller.insert(TABLE_NAME, values);
+            helper.getWritableDatabase().insert(TABLE_NAME, null, values);
         }
     }
 
+
     @Test
     public void testRemoveScreenIdGaps_firstScreenEmpty() {
         runRemoveScreenIdGapsTest(
@@ -218,7 +216,7 @@
     }
 
     private void runRemoveScreenIdGapsTest(int[] screenIds, int[] expectedScreenIds) {
-        SQLiteDatabase db = new MyModelDbController(42).getDb();
+        SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
         // Add some mock data
         for (int i = 0; i < screenIds.length; i++) {
             ContentValues values = new ContentValues();
@@ -256,15 +254,14 @@
         }
     }
 
-    private class MyModelDbController extends ModelDbController {
+    private class MyDatabaseHelper extends DatabaseHelper {
 
-        public final LongSparseArray<UserHandle> users = new LongSparseArray<>();
+        public final LongSparseArray<UserHandle> users;
 
-        MyModelDbController(long profileId) {
-            super(getInstrumentation().getTargetContext());
+        MyDatabaseHelper(long profileId) {
+            super(getInstrumentation().getTargetContext(), null, false);
+            users = new LongSparseArray<>();
             users.put(profileId, myUserHandle());
-            mOpenHelper = new DatabaseHelper(getInstrumentation().getTargetContext(), null,
-                    this::getSerialNumberForUser, () -> { });
         }
 
         @Override
@@ -272,5 +269,10 @@
             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 6fca965..bf31e39 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -363,6 +363,12 @@
         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
      */
@@ -420,7 +426,7 @@
         }
 
         public SQLiteDatabase getDb() {
-            return getModelDbController().getDb();
+            return getModelDbController().getDatabaseHelper().getWritableDatabase();
         }
     }
 
diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
index 76492ba..8fc4481 100644
--- a/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java
@@ -147,7 +147,7 @@
 
             widgetItems.add(new WidgetItem(
                     LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
-                    mTestProfile, mIconCache));
+                    mTestProfile, mIconCache, mContext));
         }
         return widgetItems;
     }
diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index e0101f5..60590e7 100644
--- a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -144,7 +144,7 @@
 
             widgetItems.add(new WidgetItem(
                     LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
-                    mTestProfile, mIconCache));
+                    mTestProfile, mIconCache, mContext));
         }
         return widgetItems;
     }
diff --git a/tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
index d8f1f14..7552619 100644
--- a/tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java
@@ -26,6 +26,7 @@
 
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
+import android.content.Context;
 import android.os.UserHandle;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -249,12 +250,13 @@
         String label = mWidgetsToLabels.get(componentName);
         AppWidgetProviderInfo widgetInfo = createAppWidgetProviderInfo(componentName);
 
+        Context context = getApplicationContext();
         LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
-                LauncherAppWidgetProviderInfo.fromProviderInfo(getApplicationContext(), widgetInfo);
+                LauncherAppWidgetProviderInfo.fromProviderInfo(context, widgetInfo);
         launcherAppWidgetProviderInfo.spanX = spanX;
         launcherAppWidgetProviderInfo.spanY = spanY;
         launcherAppWidgetProviderInfo.label = label;
 
-        return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache);
+        return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache, context);
     }
 }
diff --git a/tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java b/tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
index 0124f73..9c03ccf 100644
--- a/tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/search/SimpleWidgetsSearchAlgorithmTest.java
@@ -202,7 +202,7 @@
 
             WidgetItem widgetItem = new WidgetItem(
                     LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
-                    mTestProfile, mIconCache);
+                    mTestProfile, mIconCache, mContext);
             widgetItems.add(widgetItem);
         }
         return widgetItems;
diff --git a/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java b/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
index 834f851..2c5a396 100644
--- a/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/util/WidgetsTableUtilsTest.java
@@ -218,19 +218,18 @@
                 new Point(2, 4), new Point(4, 4));
 
         ArrayList<WidgetItem> widgetItems = new ArrayList<>();
-        widgetSizes.stream().forEach(
-                widgetSize -> {
-                    AppWidgetProviderInfo info = createAppWidgetProviderInfo(
-                            ComponentName.createRelative(
-                                    TEST_PACKAGE,
-                                    ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y));
-                    LauncherAppWidgetProviderInfo widgetInfo =
-                            LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
-                    widgetInfo.spanX = widgetSize.x;
-                    widgetInfo.spanY = widgetSize.y;
-                    widgetItems.add(new WidgetItem(widgetInfo, mTestInvariantProfile, mIconCache));
-                }
-        );
+        widgetSizes.stream().forEach(widgetSize -> {
+            AppWidgetProviderInfo info = createAppWidgetProviderInfo(
+                    ComponentName.createRelative(
+                            TEST_PACKAGE,
+                            ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y));
+            LauncherAppWidgetProviderInfo widgetInfo =
+                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, info);
+            widgetInfo.spanX = widgetSize.x;
+            widgetInfo.spanY = widgetSize.y;
+            widgetItems.add(new WidgetItem(
+                    widgetInfo, mTestInvariantProfile, mIconCache, mContext));
+        });
         mWidget1x1 = widgetItems.get(0);
         mWidget2x2 = widgetItems.get(1);
         mWidget2x3 = widgetItems.get(2);
diff --git a/tests/tapl/com/android/launcher3/tapl/AppIcon.java b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
index 2687b28..0a0cf07 100644
--- a/tests/tapl/com/android/launcher3/tapl/AppIcon.java
+++ b/tests/tapl/com/android/launcher3/tapl/AppIcon.java
@@ -37,7 +37,8 @@
     }
 
     static BySelector getAppIconSelector(String appName, LauncherInstrumentation launcher) {
-        return By.clazz(TextView.class).text(appName).pkg(launcher.getLauncherPackageName());
+        return By.clazz(TextView.class).textContains(appName)
+                .pkg(launcher.getLauncherPackageName());
     }
 
     static BySelector getAnyAppIconSelector() {
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 58d5a36..a59eff7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -25,13 +25,17 @@
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_SHELL_DRAG_READY;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT;
+import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_SCALE;
 
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Condition;
+import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
@@ -43,6 +47,18 @@
     // More drag steps than Launchables to give the window manager time to register the drag.
     private static final int DEFAULT_DRAG_STEPS = 35;
 
+    // UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
+    private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
+
+    private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
+            device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
+                    TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
+                    < 0.00001f;
+
+    private final Condition<UiDevice, Boolean> mStashedTaskbarDefaultScaleCondition =
+            device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
+                    TestProtocol.TEST_INFO_RESPONSE_FIELD) - 1f < 0.00001f;
+
     LaunchedAppState(LauncherInstrumentation launcher) {
         super(launcher);
     }
@@ -187,4 +203,89 @@
             }
         }
     }
+
+    /**
+     * Emulate the cursor hovering the screen edge to unstash the taskbar.
+     *
+     * <p>This unstashing occurs when not actively hovering the taskbar.
+     */
+    public void hoverScreenBottomEdgeToUnstashTaskbar() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering screen edge to unstash taskbar")) {
+            mLauncher.getDevice().wait(mStashedTaskbarDefaultScaleCondition,
+                    ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT);
+
+            long downTime = SystemClock.uptimeMillis();
+            int leftEdge = 10;
+            Point taskbarUnstashArea = new Point(leftEdge, mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+            mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+        }
+    }
+
+    /**
+     * Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
+     */
+    public void hoverBelowHintedTaskbarToUnstash() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                         "cursor hover enter below taskbar to unstash")) {
+                downTime = SystemClock.uptimeMillis();
+                Point taskbarUnstashArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                        mLauncher.getRealDisplaySize().y - 1);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+                mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+            }
+        }
+    }
+
+    /**
+     * Emulate the cursor entering and exiting a hover over the taskbar.
+     */
+    public void hoverToShowTaskbarUnstashHint() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                         "cursor hover exiting stashed taskbar")) {
+                Point outsideStashedTaskbarHintArea = new Point(
+                        mLauncher.getRealDisplaySize().x / 2,
+                        mLauncher.getRealDisplaySize().y - 500);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(outsideStashedTaskbarHintArea.x, outsideStashedTaskbarHintArea.y),
+                        null);
+
+                mLauncher.getDevice().wait(mStashedTaskbarDefaultScaleCondition,
+                        LauncherInstrumentation.WAIT_TIME_MS);
+            }
+        }
+    }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index c4f8269..80fded5 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -107,6 +107,8 @@
     static final Pattern EVENT_TOUCH_DOWN_TIS = getTouchEventPatternTIS("ACTION_DOWN");
     static final Pattern EVENT_TOUCH_UP_TIS = getTouchEventPatternTIS("ACTION_UP");
     static final Pattern EVENT_TOUCH_CANCEL_TIS = getTouchEventPatternTIS("ACTION_CANCEL");
+    static final Pattern EVENT_HOVER_ENTER_TIS = getTouchEventPatternTIS("ACTION_HOVER_ENTER");
+    static final Pattern EVENT_HOVER_EXIT_TIS = getTouchEventPatternTIS("ACTION_HOVER_EXIT");
 
     private static final Pattern EVENT_KEY_BACK_DOWN =
             getKeyEventPattern("ACTION_DOWN", "KEYCODE_BACK");
@@ -140,7 +142,9 @@
      * Represents a point in the code at which a callback can run.
      */
     public enum CALLBACK_RUN_POINT {
-        CALLBACK_HOLD_BEFORE_DROP
+        CALLBACK_HOLD_BEFORE_DROP,
+        CALLBACK_HOVER_ENTER,
+        CALLBACK_HOVER_EXIT,
     }
 
     private Consumer<CALLBACK_RUN_POINT> mCallbackAtRunPoint = null;
@@ -1682,6 +1686,12 @@
                                     ? EVENT_TOUCH_CANCEL_TIS : EVENT_TOUCH_UP_TIS);
                 }
                 break;
+            case MotionEvent.ACTION_HOVER_ENTER:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_HOVER_ENTER_TIS);
+                break;
+            case MotionEvent.ACTION_HOVER_EXIT:
+                expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_HOVER_EXIT_TIS);
+                break;
         }
 
         final MotionEvent event = getMotionEvent(downTime, currentTime, action, point.x, point.y);