Merge "Set use_resource_processor: false" into main
diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index 97e56b7..b846604 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -26,4 +26,11 @@
     namespace: "launcher_search"
     description: "This flag enables addition of App Installer button in Private Space container."
     bug: "308064949"
-}
\ No newline at end of file
+}
+
+flag {
+    name: "private_space_restrict_accessibility_drag"
+    namespace: "launcher_search"
+    description: "This flag disables accessibility drag for Private Space Apps."
+    bug: "289223923"
+}
diff --git a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
index 667f784..0ce1cb8 100644
--- a/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
+++ b/quickstep/src/com/android/launcher3/model/QuickstepModelDelegate.java
@@ -68,6 +68,7 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.IntSparseArrayMap;
@@ -553,7 +554,10 @@
                     if (lai == null) {
                         return null;
                     }
-                    AppInfo info = new AppInfo(lai, user, mUMS.isUserQuiet(user));
+                    AppInfo info = new AppInfo(
+                            lai,
+                            UserCache.INSTANCE.get(mAppState.getContext()).getUserInfo(user),
+                            mUMS.isUserQuiet(user));
                     info.container = mContainer;
                     mAppState.getIconCache().getTitleAndIcon(info, lai, false);
                     mReadCount++;
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 4e84f4a..2e40117 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -196,12 +196,13 @@
                 remoteTargetHandles = gluer.assignTargets(targets);
             }
         }
+
         final int recentsActivityRotation =
                 recentsView.getPagedViewOrientedState().getRecentsActivityRotation();
-        for (RemoteTargetHandle remoteTargetGluer : remoteTargetHandles) {
-            remoteTargetGluer.getTaskViewSimulator().getOrientationState().setRecentsRotation(
-                    recentsActivityRotation);
-            remoteTargetGluer.getTransformParams().setSyncTransactionApplier(applier);
+        for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
+            remoteTargetHandle.getTaskViewSimulator().getOrientationState()
+                    .setRecentsRotation(recentsActivityRotation);
+            remoteTargetHandle.getTransformParams().setSyncTransactionApplier(applier);
         }
 
         int taskIndex = recentsView.indexOfChild(v);
@@ -394,6 +395,13 @@
 
         out.addListener(new AnimationSuccessListener() {
             @Override
+            public void onAnimationStart(Animator animation) {
+                for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) {
+                    remoteTargetHandle.getTaskViewSimulator().setDrawsBelowRecents(false);
+                }
+            }
+
+            @Override
             public void onAnimationSuccess(Animator animator) {
                 if (isQuickSwitch) {
                     InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH);
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index a36f501..4198e2d 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -54,6 +54,8 @@
     static final String KEY_TUTORIAL_TYPE = "tutorial_type";
     static final String KEY_GESTURE_COMPLETE = "gesture_complete";
     static final String KEY_USE_TUTORIAL_MENU = "use_tutorial_menu";
+    public static final double SQUARE_ASPECT_RATIO_BOTTOM_BOUND = 0.95;
+    public static final double SQUARE_ASPECT_RATIO_UPPER_BOUND = 1.05;
 
     @Nullable private TutorialType[] mTutorialSteps;
     private GestureSandboxFragment mCurrentFragment;
@@ -159,14 +161,20 @@
      * Gesture animations are only in landscape for large screens and portrait for mobile. This
      * method enforces the following flows:
      *     1) phone / two-panel closed -> lock to portrait
-     *     2) two-panel open / tablet + portrait -> prompt the user to rotate the screen
-     *     3) two-panel open / tablet + landscape -> hide potential rotating prompt
+     *     2) Large screen + portrait -> prompt the user to rotate the screen
+     *     3) Large screen + landscape -> hide potential rotating prompt
+     *     4) Square aspect ratio -> no action taken as the animations will fit both orientations
      */
     private void correctUserOrientation() {
         DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
                 getApplicationContext()).getDeviceProfile(this);
         if (deviceProfile.isTablet) {
-            boolean showRotationPrompt = getResources().getConfiguration().orientation
+            // The tutorial will work in either orientation if the height and width are similar
+            boolean isAspectRatioSquare =
+                    deviceProfile.aspectRatio > SQUARE_ASPECT_RATIO_BOTTOM_BOUND
+                            && deviceProfile.aspectRatio < SQUARE_ASPECT_RATIO_UPPER_BOUND;
+            boolean showRotationPrompt = !isAspectRatioSquare
+                    && getResources().getConfiguration().orientation
                     == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
 
             GestureSandboxFragment recreatedFragment =
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 66a880b..c8d631d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1034,15 +1034,6 @@
                     recentsView.getDepthController());
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
-                public void onAnimationStart(Animator animation) {
-                    recentsView.runActionOnRemoteHandles(
-                            (Consumer<RemoteTargetHandle>) remoteTargetHandle ->
-                                    remoteTargetHandle
-                                            .getTaskViewSimulator()
-                                            .setDrawsBelowRecents(false));
-                }
-
-                @Override
                 public void onAnimationEnd(Animator animator) {
                     if (mTask != null && mTask.key.displayId != getRootViewDisplayId()) {
                         launchTaskAnimated();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 1b8866a..fcb8320 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -55,7 +55,6 @@
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -334,9 +333,6 @@
 
     @Test
     @TaskbarModeSwitch
-    @ScreenRecord // b/314873201
-    // Staging; will be promoted to presubmit if stable
-    @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
     public void testQuickSwitchToPreviousAppForTablet() throws Exception {
         assumeTrue(mLauncher.isTablet());
         startTestActivity(2);
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 6d2fbb4..1b4edf8 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -16,18 +16,13 @@
 
 package com.android.launcher3;
 
-import static android.animation.ValueAnimator.areAnimatorsEnabled;
-
-import static com.android.app.animation.Interpolators.DECELERATE_1_5;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON;
 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
-import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET;
 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -70,6 +65,7 @@
 import com.android.launcher3.celllayout.ItemConfiguration;
 import com.android.launcher3.celllayout.ReorderAlgorithm;
 import com.android.launcher3.celllayout.ReorderParameters;
+import com.android.launcher3.celllayout.ReorderPreviewAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.PreviewBackground;
@@ -188,7 +184,7 @@
 
     @ContainerType private final int mContainerType;
 
-    private final float mChildScale = 1f;
+    public static final float DEFAULT_SCALE = 1f;
 
     public static final int MODE_SHOW_REORDER_HINT = 0;
     public static final int MODE_DRAG_OVER = 1;
@@ -198,8 +194,8 @@
     private static final boolean DESTRUCTIVE_REORDER = false;
     private static final boolean DEBUG_VISUALIZE_OCCUPIED = false;
 
-    private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
-    private static final int REORDER_ANIMATION_DURATION = 150;
+    public static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
+    public static final int REORDER_ANIMATION_DURATION = 150;
     @Thunk final float mReorderPreviewAnimationMagnitude;
 
     private final ArrayList<View> mIntersectingViews = new ArrayList<>();
@@ -761,8 +757,8 @@
             bubbleChild.setTextVisibility(mContainerType != HOTSEAT);
         }
 
-        child.setScaleX(mChildScale);
-        child.setScaleY(mChildScale);
+        child.setScaleX(DEFAULT_SCALE);
+        child.setScaleY(DEFAULT_SCALE);
 
         // Generate an id for each view, this assumes we have at most 256x256 cells
         // per workspace screen
@@ -1438,147 +1434,14 @@
 
             CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams();
             if (c != null && !skip && (child instanceof Reorderable)) {
-                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child,
-                        mode, lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY);
+                ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode,
+                        lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY,
+                        mReorderPreviewAnimationMagnitude, this, mShakeAnimators);
                 rha.animate();
             }
         }
     }
 
-    // Class which represents the reorder preview animations. These animations show that an item is
-    // in a temporary state, and hint at where the item will return to.
-    class ReorderPreviewAnimation<T extends View & Reorderable> implements
-            ValueAnimator.AnimatorUpdateListener {
-        final T child;
-        float finalDeltaX;
-        float finalDeltaY;
-        float initDeltaX;
-        float initDeltaY;
-        final float finalScale;
-        float initScale;
-        private static final int PREVIEW_DURATION = 300;
-        private static final int HINT_DURATION = Workspace.REORDER_TIMEOUT;
-
-        private static final float CHILD_DIVIDEND = 4.0f;
-
-        public static final int MODE_HINT = 0;
-        public static final int MODE_PREVIEW = 1;
-
-        ValueAnimator mAnimator;
-
-        ReorderPreviewAnimation(View childView, int mode, int cellX0, int cellY0,
-                int cellX1, int cellY1, int spanX, int spanY) {
-            regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint);
-            final int x0 = mTmpPoint[0];
-            final int y0 = mTmpPoint[1];
-            regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint);
-            final int x1 = mTmpPoint[0];
-            final int y1 = mTmpPoint[1];
-            final int dX = x1 - x0;
-            final int dY = y1 - y0;
-
-            this.child = (T) childView;
-            finalDeltaX = 0;
-            finalDeltaY = 0;
-
-            MultiTranslateDelegate mtd = child.getTranslateDelegate();
-            initDeltaX = mtd.getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).getValue();
-            initDeltaY = mtd.getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).getValue();
-            initScale = child.getReorderBounceScale();
-            finalScale = mChildScale - (CHILD_DIVIDEND / child.getWidth()) * initScale;
-
-            mAnimator = ObjectAnimator.ofFloat(0, 1);
-            mAnimator.addUpdateListener(this);
-
-            // Animations are disabled in power save mode, causing the repeated animation to jump
-            // spastically between beginning and end states. Since this looks bad, we don't repeat
-            // the animation in power save mode.
-            if (areAnimatorsEnabled() && mode == MODE_PREVIEW) {
-                mAnimator.setRepeatCount(ValueAnimator.INFINITE);
-                mAnimator.setRepeatMode(ValueAnimator.REVERSE);
-            }
-
-            mAnimator.setDuration(mode == MODE_HINT ? HINT_DURATION : PREVIEW_DURATION);
-            mAnimator.setStartDelay((int) (Math.random() * 60));
-
-            int dir = mode == MODE_HINT ? -1 : 1;
-            if (dX == dY && dX == 0) {
-            } else {
-                if (dY == 0) {
-                    finalDeltaX = -dir * Math.signum(dX) * mReorderPreviewAnimationMagnitude;
-                } else if (dX == 0) {
-                    finalDeltaY = -dir * Math.signum(dY) * mReorderPreviewAnimationMagnitude;
-                } else {
-                    double angle = Math.atan( (float) (dY) / dX);
-                    finalDeltaX = (int) (-dir * Math.signum(dX)
-                            * Math.abs(Math.cos(angle) * mReorderPreviewAnimationMagnitude));
-                    finalDeltaY = (int) (-dir * Math.signum(dY)
-                            * Math.abs(Math.sin(angle) * mReorderPreviewAnimationMagnitude));
-                }
-            }
-        }
-
-        void setInitialAnimationValuesToBaseline() {
-            initScale = mChildScale;
-            initDeltaX = 0;
-            initDeltaY = 0;
-        }
-
-        void animate() {
-            boolean noMovement = (finalDeltaX == 0) && (finalDeltaY == 0);
-
-            if (mShakeAnimators.containsKey(child)) {
-                ReorderPreviewAnimation oldAnimation = mShakeAnimators.get(child);
-                mShakeAnimators.remove(child);
-
-                if (noMovement) {
-                    // A previous animation for this item exists, and no new animation will exist.
-                    // Finish the old animation smoothly.
-                    oldAnimation.finishAnimation();
-                    return;
-                } else {
-                    // A previous animation for this item exists, and a new one will exist. Stop
-                    // the old animation in its tracks, and proceed with the new one.
-                    oldAnimation.cancel();
-                }
-            }
-            if (noMovement) {
-                return;
-            }
-
-            mShakeAnimators.put(child, this);
-            mAnimator.start();
-        }
-
-        @Override
-        public void onAnimationUpdate(ValueAnimator updatedAnimation) {
-            float progress = (float) updatedAnimation.getAnimatedValue();
-            child.getTranslateDelegate().setTranslation(
-                    INDEX_REORDER_BOUNCE_OFFSET,
-                    /* dx = */ progress * finalDeltaX + (1 - progress) * initDeltaX,
-                    /* dy = */ progress * finalDeltaY + (1 - progress) * initDeltaY
-            );
-            child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale);
-        }
-
-        private void cancel() {
-            mAnimator.cancel();
-        }
-
-        /**
-         * Smoothly returns the item to its baseline position / scale
-         */
-        @Thunk void finishAnimation() {
-            mAnimator.cancel();
-            setInitialAnimationValuesToBaseline();
-            mAnimator = ObjectAnimator.ofFloat((Float) mAnimator.getAnimatedValue(), 0);
-            mAnimator.addUpdateListener(this);
-            mAnimator.setInterpolator(DECELERATE_1_5);
-            mAnimator.setDuration(REORDER_ANIMATION_DURATION);
-            mAnimator.start();
-        }
-    }
-
     private void completeAndClearReorderPreviewAnimations() {
         for (ReorderPreviewAnimation a: mShakeAnimators.values()) {
             a.finishAnimation();
diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java
index a13dcc1..7ec0a89 100644
--- a/src/com/android/launcher3/FastScrollRecyclerView.java
+++ b/src/com/android/launcher3/FastScrollRecyclerView.java
@@ -197,11 +197,11 @@
     /**
      * Scrolls this recycler view to the bottom with easing and duration.
      */
-    public void scrollToBottomWithMotion() {
+    public void scrollToBottomWithMotion(int duration) {
         if (mScrollbar != null) {
             mScrollbar.reattachThumbToScroll();
         }
         // Emphasized interpolators with 500ms duration
-        smoothScrollBy(0, getAvailableScrollHeight(), Interpolators.EMPHASIZED, 500);
+        smoothScrollBy(0, getAvailableScrollHeight(), Interpolators.EMPHASIZED, duration);
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5b5d4cb..b531780 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -54,7 +54,6 @@
 import static com.android.launcher3.LauncherConstants.TraceEvents.ON_START_EVT;
 import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.EDIT_MODE;
 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
@@ -219,6 +218,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.KeyboardShortcutsDelegate;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.PackageUserKey;
@@ -246,8 +246,6 @@
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.PendingAppWidgetHostView;
 import com.android.launcher3.widget.WidgetAddFlowHandler;
-import com.android.launcher3.widget.WidgetInflater;
-import com.android.launcher3.widget.WidgetInflater.InflationResult;
 import com.android.launcher3.widget.WidgetManagerHelper;
 import com.android.launcher3.widget.custom.CustomWidgetManager;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
@@ -329,7 +327,7 @@
 
     private WidgetManagerHelper mAppWidgetManager;
     private LauncherWidgetHolder mAppWidgetHolder;
-    private WidgetInflater mWidgetInflater;
+    private ItemInflater<Launcher> mItemInflater;
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
 
@@ -515,10 +513,11 @@
         updateDisallowBack();
 
         mAppWidgetManager = new WidgetManagerHelper(this);
-        mWidgetInflater = new WidgetInflater(this);
         mAppWidgetHolder = createAppWidgetHolder();
         mAppWidgetHolder.startListening();
         mAppWidgetHolder.addProviderChangeListener(() -> refreshAndBindWidgetsForPackageUser(null));
+        mItemInflater = new ItemInflater<>(this, mAppWidgetHolder, getItemOnClickListener(),
+                mFocusHandler, new CellLayout(mWorkspace.getContext()));
 
         mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
 
@@ -1354,35 +1353,6 @@
     }
 
     /**
-     * Creates a view representing a shortcut.
-     *
-     * @param info The data structure describing the shortcut.
-     */
-    View createShortcut(WorkspaceItemInfo info) {
-        // This can be called before PagedView#pageScrollsInitialized returns true, so use the
-        // first page, which we always assume to be present.
-        return createShortcut((ViewGroup) mWorkspace.getChildAt(0), info);
-    }
-
-    /**
-     * Creates a view representing a shortcut inflated from the specified resource.
-     *
-     * @param parent The group the shortcut belongs to. This is not necessarily the group where
-     *               the shortcut should be added.
-     * @param info   The data structure describing the shortcut.
-     * @return A View inflated from layoutResId.
-     */
-    public View createShortcut(@Nullable ViewGroup parent, WorkspaceItemInfo info) {
-        BubbleTextView favorite =
-                (BubbleTextView) LayoutInflater.from(parent != null ? parent.getContext() : this)
-                        .inflate(R.layout.app_icon, parent, false);
-        favorite.applyFromWorkspaceItem(info);
-        favorite.setOnClickListener(getItemOnClickListener());
-        favorite.setOnFocusChangeListener(mFocusHandler);
-        return favorite;
-    }
-
-    /**
      * Add a shortcut to the workspace or to a Folder.
      *
      * @param data The intent describing the shortcut.
@@ -1405,7 +1375,7 @@
 
         if (container < 0) {
             // Adding a shortcut to the Workspace.
-            final View view = createShortcut(info);
+            final View view = mItemInflater.inflateItem(info, getModelWriter());
             boolean foundCellSpan = false;
             // First we check if we already know the exact location where we want to add this item.
             if (cellX >= 0 && cellY >= 0) {
@@ -1491,7 +1461,7 @@
                 itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY);
 
         hostView.setVisibility(View.VISIBLE);
-        prepareAppWidget(hostView, launcherInfo);
+        mItemInflater.prepareAppWidget(hostView, launcherInfo);
         mWorkspace.addInScreen(hostView, launcherInfo);
         announceForAccessibility(R.string.item_added_to_workspace);
 
@@ -1516,12 +1486,6 @@
         }
     }
 
-    private void prepareAppWidget(AppWidgetHostView hostView, LauncherAppWidgetInfo item) {
-        hostView.setTag(item);
-        hostView.setFocusable(true);
-        hostView.setOnFocusChangeListener(mFocusHandler);
-    }
-
     private final ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
 
     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
@@ -2157,63 +2121,19 @@
             final boolean focusFirstItemForAccessibility) {
         // Get the list of added items and intersect them with the set of items here
         final Collection<Animator> bounceAnims = new ArrayList<>();
-        boolean canAnimatePageChange = canAnimatePageChange();
         Workspace<?> workspace = mWorkspace;
         int newItemsScreenId = -1;
         int end = items.size();
         View newView = null;
         for (int i = 0; i < end; i++) {
             final ItemInfo item = items.get(i);
-            // Short circuit if we are loading dock items for a configuration which has no dock
-            if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
-                    mHotseat == null) {
-                continue;
-            }
 
-            final View view;
-            switch (item.itemType) {
-                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
-                    WorkspaceItemInfo info = (WorkspaceItemInfo) item;
-                    view = createShortcut(info);
-                    break;
-                }
-                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
-                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this,
-                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
-                            (FolderInfo) item);
-                    break;
-                }
-                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: {
-                    view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, this,
-                            (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
-                            (FolderInfo) item);
-                    break;
-                }
-                case ITEM_TYPE_APPWIDGET:
-                case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
-                    view = inflateAppWidget((LauncherAppWidgetInfo) item);
-                    if (view == null) {
-                        continue;
-                    }
-                    break;
-                }
-                default:
-                    throw new RuntimeException("Invalid Item Type");
-            }
-
-            /*
-             * Remove colliding items.
-             */
+            // Remove colliding items.
             CellPos presenterPos = getCellPosMapper().mapModelToPresenter(item);
             if (item.container == CONTAINER_DESKTOP) {
                 CellLayout cl = mWorkspace.getScreenWithId(presenterPos.screenId);
                 if (cl != null && cl.isOccupied(presenterPos.cellX, presenterPos.cellY)) {
-                    View v = cl.getChildAt(presenterPos.cellX, presenterPos.cellY);
-                    if (v == null) {
-                        Log.e(TAG, "bindItems failed when removing colliding item=" + item);
-                    }
-                    Object tag = v.getTag();
+                    Object tag = cl.getChildAt(presenterPos.cellX, presenterPos.cellY).getTag();
                     String desc = "Collision while binding workspace item: " + item
                             + ". Collides with " + tag;
                     if (FeatureFlags.IS_STUDIO_BUILD) {
@@ -2224,6 +2144,11 @@
                     }
                 }
             }
+
+            final View view = mItemInflater.inflateItem(item, getModelWriter());
+            if (view == null) {
+                continue;
+            }
             workspace.addInScreenFromBind(view, item);
             if (forceAnimateIcons) {
                 // Animate all the applications up now
@@ -2240,7 +2165,7 @@
         }
 
         View viewToFocus = newView;
-        // Animate to the correct pager
+        // Animate to the correct page
         if (forceAnimateIcons && newItemsScreenId > -1) {
             AnimatorSet anim = new AnimatorSet();
             anim.playTogether(bounceAnims);
@@ -2257,19 +2182,13 @@
             final int newScreenIndex = mWorkspace.getPageIndexForScreenId(newItemsScreenId);
             final Runnable startBounceAnimRunnable = anim::start;
 
-            if (canAnimatePageChange && newItemsScreenId != currentScreenId) {
+            if (canAnimatePageChange() && newItemsScreenId != currentScreenId) {
                 // We post the animation slightly delayed to prevent slowdowns
                 // when we are loading right after we return to launcher.
-                mWorkspace.postDelayed(new Runnable() {
-                    public void run() {
-                        if (mWorkspace != null) {
-                            closeOpenViews(false);
-
-                            mWorkspace.snapToPage(newScreenIndex);
-                            mWorkspace.postDelayed(startBounceAnimRunnable,
-                                    NEW_APPS_ANIMATION_DELAY);
-                        }
-                    }
+                mWorkspace.postDelayed(() -> {
+                    closeOpenViews(false);
+                    mWorkspace.snapToPage(newScreenIndex);
+                    mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY);
                 }, NEW_APPS_PAGE_MOVE_DELAY);
             } else {
                 mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY);
@@ -2284,36 +2203,13 @@
      * Add the views for a widget to the workspace.
      */
     public void bindAppWidget(LauncherAppWidgetInfo item) {
-        View view = inflateAppWidget(item);
+        View view = mItemInflater.inflateItem(item, getModelWriter());
         if (view != null) {
             mWorkspace.addInScreen(view, item);
             mWorkspace.requestLayout();
         }
     }
 
-    private View inflateAppWidget(LauncherAppWidgetInfo item) {
-        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
-        try {
-            InflationResult inflationResult = mWidgetInflater.inflateAppWidget(item);
-            if (inflationResult.getType() == WidgetInflater.TYPE_DELETE) {
-                getModelWriter().deleteItemFromDatabase(item, inflationResult.getReason());
-                return null;
-            }
-
-            if (inflationResult.isUpdate()) {
-                getModelWriter().updateItemInDatabase(item);
-            }
-            AppWidgetHostView view = inflationResult.getType() == WidgetInflater.TYPE_PENDING
-                    ? new PendingAppWidgetHostView(this, item, inflationResult.getWidgetInfo())
-                    : mAppWidgetHolder.createView(
-                            item.appWidgetId, inflationResult.getWidgetInfo());
-            prepareAppWidget(view, item);
-            return view;
-        } finally {
-            TraceHelper.INSTANCE.endSection();
-        }
-    }
-
     /**
      * Restores a pending widget.
      *
@@ -3096,6 +2992,10 @@
         return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
     }
 
+    public ItemInflater<Launcher> getItemInflater() {
+        return mItemInflater;
+    }
+
     /**
      * Returns the current popup for testing, if any.
      */
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index be4168d..2eff8aa 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -73,7 +73,6 @@
 import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
 import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.celllayout.CellInfo;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
 import com.android.launcher3.celllayout.CellPosMapper;
@@ -99,7 +98,6 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemFactory;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.statemanager.StateManager;
@@ -2339,10 +2337,6 @@
         }
     }
 
-    public CellLayout getCurrentDragOverlappingLayout() {
-        return mDragOverlappingLayout;
-    }
-
     void setCurrentDropOverCell(int x, int y) {
         if (x != mDragOverX || y != mDragOverY) {
             mDragOverX = x;
@@ -2854,36 +2848,9 @@
         } else {
             // This is for other drag/drop cases, like dragging from All Apps
             mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
-            View view;
-
-            switch (info.itemType) {
-                case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-                case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
-                case LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION:
-                    if (info instanceof WorkspaceItemFactory) {
-                        // Came from all apps -- make a copy
-                        info = ((WorkspaceItemFactory) info).makeWorkspaceItem(mLauncher);
-                        d.dragInfo = info;
-                    }
-                    if (info instanceof WorkspaceItemInfo
-                            && info.container == LauncherSettings.Favorites.CONTAINER_PREDICTION) {
-                        // Came from all apps prediction row -- make a copy
-                        info = new WorkspaceItemInfo((WorkspaceItemInfo) info);
-                        d.dragInfo = info;
-                    }
-                    view = mLauncher.createShortcut(cellLayout, (WorkspaceItemInfo) info);
-                    break;
-                case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-                    view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, mLauncher, cellLayout,
-                            (FolderInfo) info);
-                    break;
-                case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
-                    view = AppPairIcon.inflateIcon(R.layout.app_pair_icon, mLauncher, cellLayout,
-                            (FolderInfo) info);
-                    break;
-                default:
-                    throw new IllegalStateException("Unknown item type: " + info.itemType);
-            }
+            View view = mLauncher.getItemInflater()
+                    .inflateItem(info, mLauncher.getModelWriter(), cellLayout);
+            d.dragInfo = info = (ItemInfo) view.getTag();
 
             // First we find the cell nearest to point at which the item is
             // dropped, without any consideration to whether there is an item there.
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index ac5b528..0844275 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.keyboard.KeyboardDragAndDropView;
+import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
@@ -126,7 +127,8 @@
     }
 
     private boolean supportAddToWorkSpace(ItemInfo item) {
-        return (item instanceof WorkspaceItemFactory)
+        return ((item instanceof AppInfo)
+                    && (((AppInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
                 || ((item instanceof WorkspaceItemInfo)
                     && (((WorkspaceItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
                 || ((item instanceof PendingAddItemInfo)
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 55438fe..930196d 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -126,6 +126,7 @@
     public static final float PULL_MULTIPLIER = .02f;
     public static final float FLING_VELOCITY_MULTIPLIER = 1200f;
     protected static final String BUNDLE_KEY_CURRENT_PAGE = "launcher.allapps.current_page";
+    private static final int SCROLL_TO_BOTTOM_DURATION = 500;
     private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
     // Render the header protection at all times to debug clipping issues.
     private static final boolean DEBUG_HEADER_PROTECTION = false;
@@ -515,7 +516,7 @@
             // Switch to the main tab
             switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN);
             // Scroll to bottom
-            getActiveRecyclerView().scrollToBottomWithMotion();
+            getActiveRecyclerView().scrollToBottomWithMotion(SCROLL_TO_BOTTOM_DURATION);
         });
     }
 
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index bc3269d..bc55597 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -120,11 +120,7 @@
                 (ActivityAllAppsContainerView<?>.AdapterHolder) mAllApps.mAH.get(MAIN);
         if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()
                 && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) {
-            RecyclerViewAnimationController recyclerViewAnimationController =
-                    new RecyclerViewAnimationController(mAllApps);
-            recyclerViewAnimationController.animateToState(true /* expand */,
-                    ANIMATION_DURATION, () -> {});
-            mAllApps.getActiveRecyclerView().scrollToBottomWithMotion();
+            mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(ANIMATION_DURATION);
         }
     }
 
diff --git a/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt b/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt
new file mode 100644
index 0000000..62b19d4
--- /dev/null
+++ b/src/com/android/launcher3/celllayout/ReorderPreviewAnimation.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout
+
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.areAnimatorsEnabled
+import android.util.ArrayMap
+import android.view.View
+import com.android.app.animation.Interpolators.DECELERATE_1_5
+import com.android.launcher3.CellLayout
+import com.android.launcher3.CellLayout.REORDER_ANIMATION_DURATION
+import com.android.launcher3.Reorderable
+import com.android.launcher3.Workspace
+import com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET
+import com.android.launcher3.util.Thunk
+import kotlin.math.abs
+import kotlin.math.atan
+import kotlin.math.cos
+import kotlin.math.sign
+import kotlin.math.sin
+
+/**
+ * Class which represents the reorder preview animations. These animations show that an item is in a
+ * temporary state, and hint at where the item will return to.
+ */
+class ReorderPreviewAnimation<T>(
+    val child: T,
+    // If the mode is MODE_HINT it will only move one period and stop, it then will be going
+    // backwards to the initial position, otherwise it will oscillate.
+    val mode: Int,
+    cellX0: Int,
+    cellY0: Int,
+    cellX1: Int,
+    cellY1: Int,
+    spanX: Int,
+    spanY: Int,
+    reorderMagnitude: Float,
+    cellLayout: CellLayout,
+    private val shakeAnimators: ArrayMap<Reorderable, ReorderPreviewAnimation<T>>
+) : ValueAnimator.AnimatorUpdateListener where T : View, T : Reorderable {
+
+    private var finalDeltaX = 0f
+    private var finalDeltaY = 0f
+    private var initDeltaX =
+        child.getTranslateDelegate().getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).value
+    private var initDeltaY =
+        child.getTranslateDelegate().getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).value
+    private var initScale = child.getReorderBounceScale()
+    private val finalScale = CellLayout.DEFAULT_SCALE - CHILD_DIVIDEND / child.width * initScale
+
+    private val dir = if (mode == MODE_HINT) -1 else 1
+    var animator: ValueAnimator =
+        ObjectAnimator.ofFloat(0f, 1f).also {
+            it.addUpdateListener(this)
+            it.setDuration((if (mode == MODE_HINT) HINT_DURATION else PREVIEW_DURATION).toLong())
+            it.startDelay = (Math.random() * 60).toLong()
+            // Animations are disabled in power save mode, causing the repeated animation to jump
+            // spastically between beginning and end states. Since this looks bad, we don't repeat
+            // the animation in power save mode.
+            if (areAnimatorsEnabled() && mode == MODE_PREVIEW) {
+                it.repeatCount = ValueAnimator.INFINITE
+                it.repeatMode = ValueAnimator.REVERSE
+            }
+        }
+
+    init {
+        val tmpRes = intArrayOf(0, 0)
+        cellLayout.regionToCenterPoint(cellX0, cellY0, spanX, spanY, tmpRes)
+        val (x0, y0) = tmpRes
+        cellLayout.regionToCenterPoint(cellX1, cellY1, spanX, spanY, tmpRes)
+        val (x1, y1) = tmpRes
+        val dX = x1 - x0
+        val dY = y1 - y0
+
+        if (dX != 0 || dY != 0) {
+            if (dY == 0) {
+                finalDeltaX = -dir * sign(dX.toFloat()) * reorderMagnitude
+            } else if (dX == 0) {
+                finalDeltaY = -dir * sign(dY.toFloat()) * reorderMagnitude
+            } else {
+                val angle = atan((dY.toFloat() / dX))
+                finalDeltaX = (-dir * sign(dX.toFloat()) * abs(cos(angle) * reorderMagnitude))
+                finalDeltaY = (-dir * sign(dY.toFloat()) * abs(sin(angle) * reorderMagnitude))
+            }
+        }
+    }
+
+    private fun setInitialAnimationValuesToBaseline() {
+        initScale = CellLayout.DEFAULT_SCALE
+        initDeltaX = 0f
+        initDeltaY = 0f
+    }
+
+    fun animate() {
+        val noMovement = finalDeltaX == 0f && finalDeltaY == 0f
+        if (shakeAnimators.containsKey(child)) {
+            val oldAnimation: ReorderPreviewAnimation<T>? = shakeAnimators.remove(child)
+            if (noMovement) {
+                // A previous animation for this item exists, and no new animation will exist.
+                // Finish the old animation smoothly.
+                oldAnimation!!.finishAnimation()
+                return
+            } else {
+                // A previous animation for this item exists, and a new one will exist. Stop
+                // the old animation in its tracks, and proceed with the new one.
+                oldAnimation!!.cancel()
+            }
+        }
+        if (noMovement) {
+            return
+        }
+        shakeAnimators[child] = this
+        animator.start()
+    }
+
+    override fun onAnimationUpdate(updatedAnimation: ValueAnimator) {
+        val progress = updatedAnimation.animatedValue as Float
+        child
+            .getTranslateDelegate()
+            .setTranslation(
+                INDEX_REORDER_BOUNCE_OFFSET,
+                /* x = */ progress * finalDeltaX + (1 - progress) * initDeltaX,
+                /* y = */ progress * finalDeltaY + (1 - progress) * initDeltaY
+            )
+        child.setReorderBounceScale(progress * finalScale + (1 - progress) * initScale)
+    }
+
+    private fun cancel() {
+        animator.cancel()
+    }
+
+    /** Smoothly returns the item to its baseline position / scale */
+    @Thunk
+    fun finishAnimation() {
+        animator.cancel()
+        setInitialAnimationValuesToBaseline()
+        animator = ObjectAnimator.ofFloat((animator.animatedValue as Float), 0f)
+        animator.addUpdateListener(this)
+        animator.interpolator = DECELERATE_1_5
+        animator.setDuration(REORDER_ANIMATION_DURATION.toLong())
+        animator.start()
+    }
+
+    companion object {
+        const val PREVIEW_DURATION = 300
+        const val HINT_DURATION = Workspace.REORDER_TIMEOUT
+        private const val CHILD_DIVIDEND = 4.0f
+        const val MODE_HINT = 0
+        const val MODE_PREVIEW = 1
+    }
+}
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index 66c9109..78298b3 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -94,7 +94,8 @@
                         CellLayout cellLayout = mLauncher.getCellLayout(info.container,
                                 mLauncher.getCellPosMapper().mapModelToPresenter(info).screenId);
                         finalItem =  info.contents.remove(0);
-                        newIcon = mLauncher.createShortcut(cellLayout, finalItem);
+                        newIcon = mLauncher.getItemInflater().inflateItem(
+                                finalItem, mLauncher.getModelWriter(), cellLayout);
                         mLauncher.getModelWriter().addOrMoveItemInDatabase(finalItem,
                                 info.container, info.screenId, info.cellX, info.cellY);
                     }
diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java
index 1044dfb..2f678a8 100644
--- a/src/com/android/launcher3/model/LoaderCursor.java
+++ b/src/com/android/launcher3/model/LoaderCursor.java
@@ -50,11 +50,13 @@
 import com.android.launcher3.model.data.IconRequestInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.UserIconInfo;
 
 import java.net.URISyntaxException;
 import java.security.InvalidParameterException;
@@ -353,6 +355,8 @@
         final WorkspaceItemInfo info = new WorkspaceItemInfo();
         info.user = user;
         info.intent = newIntent;
+        UserCache userCache = UserCache.getInstance(mContext);
+        UserIconInfo userIconInfo = userCache.getUserInfo(user);
 
         if (loadIcon) {
             mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon);
@@ -362,7 +366,7 @@
         }
 
         if (mActivityInfo != null) {
-            AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo);
+            AppInfo.updateRuntimeFlagsForActivityTarget(info, mActivityInfo, userIconInfo);
         }
 
         // from the db
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index d0a1f10..736b80a 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -655,7 +655,7 @@
             // Create the ApplicationInfos
             for (int i = 0; i < apps.size(); i++) {
                 LauncherActivityInfo app = apps.get(i);
-                AppInfo appInfo = new AppInfo(app, user, quietMode);
+                AppInfo appInfo = new AppInfo(app, mUserCache.getUserInfo(user), quietMode);
                 if (enableSupportForArchiving() && app.getApplicationInfo().isArchived) {
                     // For archived apps, include progress info in case there is a pending
                     // install session post restart of device.
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index ea8a7a1..72eda6c 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -31,10 +31,13 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.PackageInstallInfo;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.UserIconInfo;
 
 import java.util.Comparator;
 
@@ -83,20 +86,21 @@
      * Must not hold the Context.
      */
     public AppInfo(Context context, LauncherActivityInfo info, UserHandle user) {
-        this(info, user, context.getSystemService(UserManager.class).isQuietModeEnabled(user));
+        this(info, UserCache.INSTANCE.get(context).getUserInfo(user),
+                context.getSystemService(UserManager.class).isQuietModeEnabled(user));
     }
 
-    public AppInfo(LauncherActivityInfo info, UserHandle user, boolean quietModeEnabled) {
+    public AppInfo(LauncherActivityInfo info, UserIconInfo userIconInfo, boolean quietModeEnabled) {
         this.componentName = info.getComponentName();
         this.container = CONTAINER_ALL_APPS;
-        this.user = user;
+        this.user = userIconInfo.user;
         intent = makeLaunchIntent(info);
 
         if (quietModeEnabled) {
             runtimeStatusFlags |= FLAG_DISABLED_QUIET_USER;
         }
         uid = info.getApplicationInfo().uid;
-        updateRuntimeFlagsForActivityTarget(this, info);
+        updateRuntimeFlagsForActivityTarget(this, info, userIconInfo);
     }
 
     public AppInfo(AppInfo info) {
@@ -170,7 +174,7 @@
     }
 
     public static void updateRuntimeFlagsForActivityTarget(
-            ItemInfoWithIcon info, LauncherActivityInfo lai) {
+            ItemInfoWithIcon info, LauncherActivityInfo lai, UserIconInfo userIconInfo) {
         ApplicationInfo appInfo = lai.getApplicationInfo();
         if (PackageManagerHelper.isAppSuspended(appInfo)) {
             info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
@@ -181,6 +185,12 @@
         info.runtimeStatusFlags |= (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0
                 ? FLAG_SYSTEM_NO : FLAG_SYSTEM_YES;
 
+        if (Flags.privateSpaceRestrictAccessibilityDrag()) {
+            if (userIconInfo.isPrivate()) {
+                info.runtimeStatusFlags |= FLAG_NOT_PINNABLE;
+            }
+        }
+
         // Sets the progress level, installation and incremental download flags.
         info.setProgressLevel(
                 PackageManagerHelper.getLoadingProgress(lai),
diff --git a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
index c67ec5a..9e169cf 100644
--- a/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
+++ b/src/com/android/launcher3/model/data/WorkspaceItemInfo.java
@@ -25,10 +25,12 @@
 
 import androidx.annotation.NonNull;
 
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.uioverrides.ApiWrapper;
 import com.android.launcher3.util.ContentWriter;
@@ -120,6 +122,11 @@
     public WorkspaceItemInfo(ShortcutInfo shortcutInfo, Context context) {
         user = shortcutInfo.getUserHandle();
         itemType = Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+        if (Flags.privateSpaceRestrictAccessibilityDrag()) {
+            if (UserCache.INSTANCE.get(context).getUserInfo(user).isPrivate()) {
+                runtimeStatusFlags |= FLAG_NOT_PINNABLE;
+            }
+        }
         updateFromDeepShortcutInfo(shortcutInfo, context);
     }
 
diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt
new file mode 100644
index 0000000..79091ca
--- /dev/null
+++ b/src/com/android/launcher3/util/ItemInflater.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.appwidget.AppWidgetHostView
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.View.OnFocusChangeListener
+import android.view.ViewGroup
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.R
+import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.folder.FolderIcon
+import com.android.launcher3.model.ModelWriter
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemFactory
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.widget.LauncherWidgetHolder
+import com.android.launcher3.widget.PendingAppWidgetHostView
+import com.android.launcher3.widget.WidgetInflater
+
+/** Utility class to inflate View for a model item */
+class ItemInflater<T>(
+    private val context: T,
+    private val widgetHolder: LauncherWidgetHolder,
+    private val clickListener: OnClickListener,
+    private val focusListener: OnFocusChangeListener,
+    private val defaultParent: ViewGroup
+) where T : Context, T : ActivityContext {
+
+    private val widgetInflater = WidgetInflater(context)
+
+    @JvmOverloads
+    fun inflateItem(item: ItemInfo, writer: ModelWriter, nullableParent: ViewGroup? = null): View? {
+        val parent = nullableParent ?: defaultParent
+        when (item.itemType) {
+            Favorites.ITEM_TYPE_APPLICATION,
+            Favorites.ITEM_TYPE_DEEP_SHORTCUT,
+            Favorites.ITEM_TYPE_SEARCH_ACTION -> {
+                var info =
+                    if (item is WorkspaceItemFactory) {
+                        (item as WorkspaceItemFactory).makeWorkspaceItem(context)
+                    } else {
+                        item as WorkspaceItemInfo
+                    }
+                if (info.container == Favorites.CONTAINER_PREDICTION) {
+                    // Came from all apps prediction row -- make a copy
+                    info = WorkspaceItemInfo(info)
+                }
+                return createShortcut(info, parent)
+            }
+            Favorites.ITEM_TYPE_FOLDER ->
+                return FolderIcon.inflateFolderAndIcon(
+                    R.layout.folder_icon,
+                    context,
+                    parent,
+                    item as FolderInfo
+                )
+            Favorites.ITEM_TYPE_APP_PAIR ->
+                return AppPairIcon.inflateIcon(
+                    R.layout.app_pair_icon,
+                    context,
+                    parent,
+                    item as FolderInfo
+                )
+            Favorites.ITEM_TYPE_APPWIDGET,
+            Favorites.ITEM_TYPE_CUSTOM_APPWIDGET ->
+                return inflateAppWidget(item as LauncherAppWidgetInfo, writer)
+            else -> throw RuntimeException("Invalid Item Type")
+        }
+    }
+
+    /**
+     * Creates a view representing a shortcut inflated from the specified resource.
+     *
+     * @param parent The group the shortcut belongs to. This is not necessarily the group where the
+     *   shortcut should be added.
+     * @param info The data structure describing the shortcut.
+     * @return A View inflated from layoutResId.
+     */
+    private fun createShortcut(info: WorkspaceItemInfo, parent: ViewGroup): View {
+        val favorite =
+            LayoutInflater.from(parent.context).inflate(R.layout.app_icon, parent, false)
+                as BubbleTextView
+        favorite.applyFromWorkspaceItem(info)
+        favorite.setOnClickListener(clickListener)
+        favorite.onFocusChangeListener = focusListener
+        return favorite
+    }
+
+    private fun inflateAppWidget(item: LauncherAppWidgetInfo, writer: ModelWriter): View? {
+        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId)
+        try {
+            val (type, reason, _, isUpdate, widgetInfo) = widgetInflater.inflateAppWidget(item)
+            if (type == WidgetInflater.TYPE_DELETE) {
+                writer.deleteItemFromDatabase(item, reason)
+                return null
+            }
+            if (isUpdate) {
+                writer.updateItemInDatabase(item)
+            }
+            val view =
+                if (type == WidgetInflater.TYPE_PENDING || widgetInfo == null)
+                    PendingAppWidgetHostView(context, item, widgetInfo)
+                else widgetHolder.createView(item.appWidgetId, widgetInfo)
+            prepareAppWidget(view, item)
+            return view
+        } finally {
+            TraceHelper.INSTANCE.endSection()
+        }
+    }
+
+    fun prepareAppWidget(hostView: AppWidgetHostView, item: LauncherAppWidgetInfo) {
+        hostView.tag = item
+        hostView.isFocusable = true
+        hostView.onFocusChangeListener = focusListener
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
index e1af774..0ff7c20 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderAlgorithmUnitTest.java
@@ -29,8 +29,6 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.MultipageCellLayout;
 import com.android.launcher3.celllayout.board.CellLayoutBoard;
 import com.android.launcher3.celllayout.board.IconPoint;
@@ -41,8 +39,7 @@
 import com.android.launcher3.util.ActivityContextWrapper;
 import com.android.launcher3.views.DoubleShadowBubbleTextView;
 
-import org.junit.After;
-import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -70,7 +67,8 @@
     private static final int TOTAL_OF_CASES_GENERATED = 300;
     private Context mApplicationContext;
 
-    private int mPrevNumColumns, mPrevNumRows;
+    @Rule
+    public UnitTestCellLayoutBuilderRule mCellLayoutBuilder = new UnitTestCellLayoutBuilderRule();
 
     /**
      * This test reads existing test cases and makes sure the CellLayout produces the same
@@ -144,34 +142,10 @@
                 (CellLayoutLayoutParams) cell.getLayoutParams(), true);
     }
 
-    public CellLayout createCellLayout(int width, int height, boolean isMulti) {
-        Context c = mApplicationContext;
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
-        // modify the device profile.
-        dp.inv.numColumns = isMulti ? width / 2 : width;
-        dp.inv.numRows = height;
-        dp.cellLayoutBorderSpacePx = new Point(0, 0);
-
-        CellLayout cl = isMulti ? new MultipageCellLayout(getWrappedContext(c, dp))
-                : new CellLayout(getWrappedContext(c, dp));
-        // I put a very large number for width and height so that all the items can fit, it doesn't
-        // need to be exact, just bigger than the sum of cell border
-        cl.measure(View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
-                View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY));
-        return cl;
-    }
-
-    private Context getWrappedContext(Context context, DeviceProfile dp) {
-        return new ActivityContextWrapper(context) {
-            public DeviceProfile getDeviceProfile() {
-                return dp;
-            }
-        };
-    }
-
     public ItemConfiguration solve(CellLayoutBoard board, int x, int y, int spanX,
             int spanY, int minSpanX, int minSpanY, boolean isMulti) {
-        CellLayout cl = createCellLayout(board.getWidth(), board.getHeight(), isMulti);
+        CellLayout cl = mCellLayoutBuilder.createCellLayout(board.getWidth(), board.getHeight(),
+                isMulti);
 
         // The views have to be sorted or the result can vary
         board.getIcons()
@@ -249,22 +223,6 @@
         }
     }
 
-    @Before
-    public void storePreviousValues() {
-        Context c = new ActivityContextWrapper(getApplicationContext());
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
-        mPrevNumColumns = dp.inv.numColumns;
-        mPrevNumRows = dp.inv.numRows;
-    }
-
-    @After
-    public void restorePreviousValues() {
-        Context c = new ActivityContextWrapper(getApplicationContext());
-        DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(c).getDeviceProfile(c).copy(c);
-        dp.inv.numColumns = mPrevNumColumns;
-        dp.inv.numRows = mPrevNumRows;
-    }
-
     private ReorderAlgorithmUnitTestCase generateRandomTestCase(
             RandomBoardGenerator boardGenerator) {
         ReorderAlgorithmUnitTestCase testCase = new ReorderAlgorithmUnitTestCase();
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt b/tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
new file mode 100644
index 0000000..0bec1b2
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/ReorderPreviewAnimationTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout
+
+import android.content.Context
+import android.util.ArrayMap
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Reorderable
+import com.android.launcher3.celllayout.ReorderPreviewAnimation.Companion.HINT_DURATION
+import com.android.launcher3.celllayout.ReorderPreviewAnimation.Companion.PREVIEW_DURATION
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.MultiTranslateDelegate
+import com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_BOUNCE_OFFSET
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+class Mock(context: Context) : Reorderable, View(context) {
+
+    init {
+        mLeft = 0
+        mRight = 100
+    }
+
+    private val translateDelegate = MultiTranslateDelegate(this)
+
+    private var scaleForReorderBounce = 1f
+    override fun getTranslateDelegate(): MultiTranslateDelegate {
+        return translateDelegate
+    }
+
+    override fun setReorderBounceScale(scale: Float) {
+        scaleForReorderBounce = scale
+    }
+
+    override fun getReorderBounceScale(): Float {
+        return scaleForReorderBounce
+    }
+
+    fun toAnimationValues(): AnimationValues {
+        return AnimationValues(
+            (translateDelegate.getTranslationX(INDEX_REORDER_BOUNCE_OFFSET).value * 100).toInt(),
+            (translateDelegate.getTranslationY(INDEX_REORDER_BOUNCE_OFFSET).value * 100).toInt(),
+            (scaleForReorderBounce * 100).toInt()
+        )
+    }
+}
+
+data class AnimationValues(val dx: Int, val dy: Int, val scale: Int)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ReorderPreviewAnimationTest {
+
+    @JvmField @Rule var cellLayoutBuilder = UnitTestCellLayoutBuilderRule()
+
+    private val applicationContext =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+    /**
+     * @param animationTime the time of the animation we will check the state against.
+     * @param mode the mode either PREVIEW_DURATION or HINT_DURATION.
+     * @param valueToMatch the state of the animation we expect to see at animationTime.
+     * @param isAfterReverse if the animation is finish and we are returning to the beginning.
+     */
+    private fun testAnimationAtGivenProgress(
+        animationTime: Int,
+        mode: Int,
+        valueToMatch: AnimationValues
+    ) {
+        val view = Mock(applicationContext)
+        val cellLayout = cellLayoutBuilder.createCellLayout(100, 100, false)
+        val map = ArrayMap<Reorderable, ReorderPreviewAnimation<Mock>>()
+        val animation =
+            ReorderPreviewAnimation(
+                view,
+                mode,
+                3,
+                3,
+                1,
+                7,
+                1,
+                1,
+                CellLayout.REORDER_PREVIEW_MAGNITUDE,
+                cellLayout,
+                map
+            )
+        // Remove delay because it's randomly generated and it can slightly change the results.
+        animation.animator.startDelay = 0
+        animation.animator.currentPlayTime = animationTime.toLong()
+        val currentValue = view.toAnimationValues()
+        assert(currentValue == valueToMatch) {
+            "The value of the animation $currentValue at $animationTime time (milliseconds) doesn't match the given value $valueToMatch"
+        }
+    }
+
+    @Test
+    fun testAnimationModePreview() {
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION * 0,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 0, dy = 0, scale = 100)
+        )
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION / 2,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 2, dy = -5, scale = 98)
+        )
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION / 3,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 1, dy = -2, scale = 99)
+        )
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 5, dy = -10, scale = 96)
+        )
+
+        // MODE_PREVIEW oscillates and goes back to 0,0
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION * 2,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 0, dy = 0, scale = 100)
+        )
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION * 99,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 5, dy = -10, scale = 96)
+        )
+        testAnimationAtGivenProgress(
+            PREVIEW_DURATION * 98,
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 0, dy = 0, scale = 100)
+        )
+        testAnimationAtGivenProgress(
+            (PREVIEW_DURATION * 1.5).toInt(),
+            ReorderPreviewAnimation.MODE_PREVIEW,
+            AnimationValues(dx = 2, dy = -5, scale = 98)
+        )
+    }
+
+    @Test
+    fun testAnimationModeHint() {
+        testAnimationAtGivenProgress(
+            HINT_DURATION * 0,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = 0, dy = 0, scale = 100)
+        )
+        testAnimationAtGivenProgress(
+            HINT_DURATION,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -5, dy = 10, scale = 96)
+        )
+        testAnimationAtGivenProgress(
+            HINT_DURATION / 2,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -2, dy = 5, scale = 98)
+        )
+        testAnimationAtGivenProgress(
+            HINT_DURATION / 3,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -1, dy = 2, scale = 99)
+        )
+        testAnimationAtGivenProgress(
+            HINT_DURATION,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -5, dy = 10, scale = 96)
+        )
+
+        // After one cycle the animationValues should always be the top values and don't cycle.
+        testAnimationAtGivenProgress(
+            HINT_DURATION * 2,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -5, dy = 10, scale = 96)
+        )
+        testAnimationAtGivenProgress(
+            HINT_DURATION * 99,
+            ReorderPreviewAnimation.MODE_HINT,
+            AnimationValues(dx = -5, dy = 10, scale = 96)
+        )
+    }
+}
diff --git a/tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt b/tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
new file mode 100644
index 0000000..0f0dd65
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/UnitTestCellLayoutBuilderRule.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout
+
+import android.content.Context
+import android.graphics.Point
+import android.view.View
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.CellLayout
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.MultipageCellLayout
+import com.android.launcher3.util.ActivityContextWrapper
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Create CellLayouts to be used in Unit testing and make sure to set the DeviceProfile back to
+ * normal.
+ */
+class UnitTestCellLayoutBuilderRule : TestWatcher() {
+
+    private var prevNumColumns = 0
+    private var prevNumRows = 0
+
+    private val applicationContext =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+    override fun starting(description: Description?) {
+        val dp = getDeviceProfile()
+        prevNumColumns = dp.inv.numColumns
+        prevNumRows = dp.inv.numRows
+    }
+
+    override fun finished(description: Description?) {
+        val dp = getDeviceProfile()
+        dp.inv.numColumns = prevNumColumns
+        dp.inv.numRows = prevNumRows
+    }
+
+    fun createCellLayout(width: Int, height: Int, isMulti: Boolean): CellLayout {
+        val dp = getDeviceProfile()
+        // modify the device profile.
+        dp.inv.numColumns = if (isMulti) width / 2 else width
+        dp.inv.numRows = height
+        dp.cellLayoutBorderSpacePx = Point(0, 0)
+        val cl =
+            if (isMulti) MultipageCellLayout(getWrappedContext(applicationContext, dp))
+            else CellLayout(getWrappedContext(applicationContext, dp))
+        // I put a very large number for width and height so that all the items can fit, it doesn't
+        // need to be exact, just bigger than the sum of cell border
+        cl.measure(
+            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeMeasureSpec(10000, View.MeasureSpec.EXACTLY)
+        )
+        return cl
+    }
+
+    private fun getDeviceProfile(): DeviceProfile =
+        InvariantDeviceProfile.INSTANCE[applicationContext].getDeviceProfile(applicationContext)
+            .copy(applicationContext)
+
+    private fun getWrappedContext(context: Context, dp: DeviceProfile): Context {
+        return object : ActivityContextWrapper(context) {
+            override fun getDeviceProfile(): DeviceProfile {
+                return dp
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index cb57918..e2ca31f 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -95,6 +95,10 @@
     @Test
     fun loadsDataProperly() =
         with(BgDataModel()) {
+            val MAIN_HANDLE = UserHandle.of(0)
+            val mockUserHandles = arrayListOf<UserHandle>(MAIN_HANDLE)
+            `when`(userCache.userProfiles).thenReturn(mockUserHandles)
+            `when`(userCache.getUserInfo(MAIN_HANDLE)).thenReturn(UserIconInfo(MAIN_HANDLE, 1))
             LoaderTask(app, bgAllAppsList, this, modelDelegate, launcherBinder)
                 .runSyncOnBackgroundThread()
             Truth.assertThat(workspaceItems.size).isAtLeast(25)