Introducing CellPosMapper which allows mapping between UI position
and model position

Test: atest CellPosMapperTest
Bug: 188081026
Change-Id: If5c6b3df5ad240317bb535c675f6ead94084238e
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 85bd2d3..94b8cd8 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -33,6 +33,7 @@
 
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.InstanceId;
@@ -268,10 +269,11 @@
 
         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mWidgetView.getLayoutParams();
         ItemInfo widgetInfo = (ItemInfo) mWidgetView.getTag();
-        lp.setCellX(widgetInfo.cellX);
-        lp.setTmpCellX(widgetInfo.cellX);
-        lp.setCellY(widgetInfo.cellY);
-        lp.setTmpCellY(widgetInfo.cellY);
+        CellPos presenterPos = mLauncher.getCellPosMapper().mapModelToPresenter(widgetInfo);
+        lp.setCellX(presenterPos.cellX);
+        lp.setTmpCellX(presenterPos.cellX);
+        lp.setCellY(presenterPos.cellY);
+        lp.setTmpCellY(presenterPos.cellY);
         lp.cellHSpan = widgetInfo.spanX;
         lp.cellVSpan = widgetInfo.spanY;
         lp.isLockedToGrid = true;
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 05b225c..d388ebc 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -62,6 +62,7 @@
 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.PreviewBackground;
@@ -267,7 +268,7 @@
         mDragCell[0] = mDragCell[1] = -1;
         mDragCellSpan[0] = mDragCellSpan[1] = -1;
         for (int i = 0; i < mDragOutlines.length; i++) {
-            mDragOutlines[i] = new CellLayoutLayoutParams(0, 0, 0, 0, -1);
+            mDragOutlines[i] = new CellLayoutLayoutParams(0, 0, 0, 0);
         }
         mDragOutlinePaint.setColor(Themes.getAttrColor(context, R.attr.workspaceTextColor));
 
@@ -1084,8 +1085,8 @@
             final int oldY = lp.y;
             lp.isLockedToGrid = true;
             if (permanent) {
-                lp.setCellX(info.cellX = cellX);
-                lp.setCellY(info.cellY = cellY);
+                lp.setCellX(cellX);
+                lp.setCellY(cellY);
             } else {
                 lp.setTmpCellX(cellX);
                 lp.setTmpCellY(cellY);
@@ -1627,20 +1628,16 @@
             // We do a null check here because the item info can be null in the case of the
             // AllApps button in the hotseat.
             if (info != null && child != dragView) {
-                final boolean requiresDbUpdate = (info.cellX != lp.getTmpCellX()
-                        || info.cellY != lp.getTmpCellY() || info.spanX != lp.cellHSpan
-                        || info.spanY != lp.cellVSpan);
+                CellPos presenterPos = mActivity.getCellPosMapper().mapModelToPresenter(info);
+                final boolean requiresDbUpdate = (presenterPos.cellX != lp.getTmpCellX()
+                        || presenterPos.cellY != lp.getTmpCellY() || info.spanX != lp.cellHSpan
+                        || info.spanY != lp.cellVSpan || presenterPos.screenId != screenId);
 
                 lp.setCellX(lp.getTmpCellX());
-                info.cellX = lp.getTmpCellX();
-                info.cellY = lp.getTmpCellY();
                 lp.setCellY(lp.getTmpCellY());
-                info.spanX = lp.cellHSpan;
-                info.spanY = lp.cellVSpan;
-
                 if (requiresDbUpdate) {
                     Launcher.cast(mActivity).getModelWriter().modifyItemInDatabase(info, container,
-                            screenId, info.cellX, info.cellY, info.spanX, info.spanY);
+                            screenId, lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan);
                 }
             }
         }
@@ -2792,7 +2789,8 @@
         if (view instanceof LauncherAppWidgetHostView
                 && view.getTag() instanceof LauncherAppWidgetInfo) {
             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
-            mOccupied.markCells(info.cellX, info.cellY, info.spanX, info.spanY, true);
+            CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
+            mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, true);
             return;
         }
         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
@@ -2805,7 +2803,8 @@
         if (view instanceof LauncherAppWidgetHostView
                 && view.getTag() instanceof LauncherAppWidgetInfo) {
             LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag();
-            mOccupied.markCells(info.cellX, info.cellY, info.spanX, info.spanY, false);
+            CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info);
+            mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, false);
             return;
         }
         if (view == null || view.getParent() != mShortcutsAndWidgets) return;
@@ -2858,13 +2857,13 @@
         final int screenId;
         final int container;
 
-        public CellInfo(View v, ItemInfo info) {
-            cellX = info.cellX;
-            cellY = info.cellY;
+        public CellInfo(View v, ItemInfo info, CellPos cellPos) {
+            cellX = cellPos.cellX;
+            cellY = cellPos.cellY;
             spanX = info.spanX;
             spanY = info.spanY;
             cell = v;
-            screenId = info.screenId;
+            screenId = cellPos.screenId;
             container = info.container;
         }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e9b9f31..2f1f59c 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -32,6 +32,7 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
@@ -135,6 +136,8 @@
 import com.android.launcher3.allapps.BaseSearchConfig;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.celllayout.CellPosMapper;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.DotInfo;
@@ -406,6 +409,8 @@
     private StringCache mStringCache;
     private BaseSearchConfig mBaseSearchConfig;
 
+    private CellPosMapper mCellPosMapper = CellPosMapper.DEFAULT;
+
     @Override
     @TargetApi(Build.VERSION_CODES.S)
     protected void onCreate(Bundle savedInstanceState) {
@@ -725,10 +730,17 @@
         }
 
         onDeviceProfileInitiated();
-        mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true, this);
+        mCellPosMapper = CellPosMapper.DEFAULT;
+        mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true,
+                mCellPosMapper, this);
         return true;
     }
 
+    @Override
+    public CellPosMapper getCellPosMapper() {
+        return mCellPosMapper;
+    }
+
     public RotationHelper getRotationHelper() {
         return mRotationHelper;
     }
@@ -794,16 +806,18 @@
      */
     private int completeAdd(
             int requestCode, Intent intent, int appWidgetId, PendingRequestArgs info) {
-        int screenId = info.screenId;
-        if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+        CellPos cellPos = getCellPosMapper().mapModelToPresenter(info);
+        int screenId = cellPos.screenId;
+        if (info.container == CONTAINER_DESKTOP) {
             // When the screen id represents an actual screen (as opposed to a rank) we make sure
             // that the drop page actually exists.
-            screenId = ensurePendingDropLayoutExists(info.screenId);
+            screenId = ensurePendingDropLayoutExists(cellPos.screenId);
         }
 
         switch (requestCode) {
             case REQUEST_CREATE_SHORTCUT:
-                completeAddShortcut(intent, info.container, screenId, info.cellX, info.cellY, info);
+                completeAddShortcut(intent, info.container, screenId,
+                        cellPos.cellX, cellPos.cellY, info);
                 announceForAccessibility(R.string.item_added_to_workspace);
                 break;
             case REQUEST_CREATE_APPWIDGET:
@@ -899,14 +913,17 @@
                         ON_ACTIVITY_RESULT_ANIMATION_DELAY, false,
                         () -> getStateManager().goToState(NORMAL));
             } else {
-                if (requestArgs.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
+                CellPos presenterPos = getCellPosMapper().mapModelToPresenter(requestArgs);
+                if (requestArgs.container == CONTAINER_DESKTOP) {
                     // When the screen id represents an actual screen (as opposed to a rank)
                     // we make sure that the drop page actually exists.
-                    requestArgs.screenId =
-                            ensurePendingDropLayoutExists(requestArgs.screenId);
+                    int newScreenId = ensurePendingDropLayoutExists(presenterPos.screenId);
+                    requestArgs.screenId = getCellPosMapper().mapPresenterToModel(
+                            presenterPos.cellX, presenterPos.cellY, newScreenId, CONTAINER_DESKTOP)
+                                    .screenId;
                 }
                 final CellLayout dropLayout =
-                        mWorkspace.getScreenWithId(requestArgs.screenId);
+                        mWorkspace.getScreenWithId(presenterPos.screenId);
 
                 dropLayout.setDropPending(true);
                 final Runnable onComplete = new Runnable() {
@@ -964,9 +981,10 @@
             setWaitingForResult(null);
 
             View v = null;
-            CellLayout layout = getCellLayout(pendingArgs.container, pendingArgs.screenId);
+            CellPos cellPos = getCellPosMapper().mapModelToPresenter(pendingArgs);
+            CellLayout layout = getCellLayout(pendingArgs.container, cellPos.screenId);
             if (layout != null) {
-                v = layout.getChildAt(pendingArgs.cellX, pendingArgs.cellY);
+                v = layout.getChildAt(cellPos.cellX, cellPos.cellY);
             }
             Intent intent = pendingArgs.getPendingIntent();
 
@@ -1002,7 +1020,8 @@
     @Thunk
     void completeTwoStageWidgetDrop(
             final int resultCode, final int appWidgetId, final PendingRequestArgs requestArgs) {
-        CellLayout cellLayout = mWorkspace.getScreenWithId(requestArgs.screenId);
+        CellLayout cellLayout = mWorkspace.getScreenWithId(
+                getCellPosMapper().mapModelToPresenter(requestArgs).screenId);
         Runnable onCompleteRunnable = null;
         int animationType = 0;
 
@@ -1493,9 +1512,9 @@
             launcherInfo.sourceContainer =
                     ((PendingRequestArgs) itemInfo).getWidgetSourceContainer();
         }
-
+        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
         getModelWriter().addItemToDatabase(launcherInfo,
-                itemInfo.container, itemInfo.screenId, itemInfo.cellX, itemInfo.cellY);
+                itemInfo.container, presenterPos.screenId, presenterPos.cellX, presenterPos.cellY);
 
         hostView.setVisibility(View.VISIBLE);
         prepareAppWidget(hostView, launcherInfo);
@@ -1505,7 +1524,7 @@
         // Show the widget resize frame.
         if (hostView instanceof LauncherAppWidgetHostView) {
             final LauncherAppWidgetHostView launcherHostView = (LauncherAppWidgetHostView) hostView;
-            CellLayout cellLayout = getCellLayout(launcherInfo.container, launcherInfo.screenId);
+            CellLayout cellLayout = getCellLayout(launcherInfo.container, presenterPos.screenId);
             if (mStateManager.getState() == NORMAL) {
                 AppWidgetResizeFrame.showForWidget(launcherHostView, cellLayout);
             } else {
@@ -1887,12 +1906,17 @@
 
     public void addPendingItem(PendingAddItemInfo info, int container, int screenId,
             int[] cell, int spanX, int spanY) {
-        info.container = container;
-        info.screenId = screenId;
-        if (cell != null) {
-            info.cellX = cell[0];
-            info.cellY = cell[1];
+        if (cell == null) {
+            CellPos modelPos = getCellPosMapper().mapPresenterToModel(0, 0, screenId, container);
+            info.screenId = modelPos.screenId;
+        } else {
+            CellPos modelPos = getCellPosMapper().mapPresenterToModel(
+                    cell[0],  cell[1], screenId, container);
+            info.screenId = modelPos.screenId;
+            info.cellX = modelPos.cellX;
+            info.cellY = modelPos.cellY;
         }
+        info.container = container;
         info.spanX = spanX;
         info.spanY = spanY;
 
@@ -2455,10 +2479,11 @@
             /*
              * Remove colliding items.
              */
-            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-                CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
-                if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
-                    View v = cl.getChildAt(item.cellX, item.cellY);
+            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);
                     }
@@ -2484,7 +2509,7 @@
                 view.setScaleX(0f);
                 view.setScaleY(0f);
                 bounceAnims.add(createNewAppBounceAnimation(view, i));
-                newItemsScreenId = item.screenId;
+                newItemsScreenId = presenterPos.screenId;
             }
 
             if (newView == null) {
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 2c6458b..4472383 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -37,6 +37,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.logging.FileLog;
@@ -165,9 +166,9 @@
 
     @NonNull
     public ModelWriter getWriter(final boolean hasVerticalHotseat, final boolean verifyChanges,
-            @Nullable final Callbacks owner) {
+            CellPosMapper cellPosMapper, @Nullable final Callbacks owner) {
         return new ModelWriter(mApp.getContext(), this, mBgDataModel,
-                hasVerticalHotseat, verifyChanges, owner);
+                hasVerticalHotseat, verifyChanges, cellPosMapper, owner);
     }
 
     @Override
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 460c658..df829f1 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -26,6 +26,7 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.SPRING_LOADED;
 import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.config.FeatureFlags.FOLDABLE_SINGLE_PAGE;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPELEFT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPERIGHT;
@@ -69,6 +70,8 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.celllayout.CellPosMapper;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.DragController;
@@ -585,7 +588,7 @@
         }
 
         int cellHSpan = mLauncher.getDeviceProfile().inv.numSearchContainerColumns;
-        CellLayoutLayoutParams lp = new CellLayoutLayoutParams(0, 0, cellHSpan, 1, FIRST_SCREEN_ID);
+        CellLayoutLayoutParams lp = new CellLayoutLayoutParams(0, 0, cellHSpan, 1);
         lp.canReorder = false;
         if (!firstPage.addViewToCellLayout(
                 mFirstPagePinnedItem, 0, R.id.search_container_workspace, lp, true)) {
@@ -642,7 +645,7 @@
         // Inflate the cell layout, but do not add it automatically so that we can get the newly
         // created CellLayout.
         CellLayout newScreen = (CellLayout) LayoutInflater.from(getContext()).inflate(
-                        R.layout.workspace_screen, this, false /* attachToRoot */);
+                    R.layout.workspace_screen, this, false /* attachToRoot */);
 
         mWorkspaceScreens.put(screenId, newScreen);
         mScreenOrder.add(insertIndex, screenId);
@@ -664,7 +667,8 @@
 
             // If the icon was dragged from Hotseat, there is no page pair
             if (isTwoPanelEnabled() && !(mDragSourceInternal.getParent() instanceof Hotseat)) {
-                int pagePairScreenId = getScreenPair(dragObject.dragInfo.screenId);
+                int pagePairScreenId = getScreenPair(getCellPosMapper().mapModelToPresenter(
+                        dragObject.dragInfo).screenId);
                 CellLayout pagePair = mWorkspaceScreens.get(pagePairScreenId);
                 dragSourceChildCount += pagePair.getShortcutsAndWidgets().getChildCount();
             }
@@ -680,7 +684,7 @@
                 lastChildOnScreen = true;
             }
             CellLayout cl = (CellLayout) mDragSourceInternal.getParent();
-            if (getLeftmostVisiblePageForIndex(indexOfChild(cl))
+            if (!FOLDABLE_SINGLE_PAGE.get() && getLeftmostVisiblePageForIndex(indexOfChild(cl))
                     == getLeftmostVisiblePageForIndex(getPageCount() - 1)) {
                 childOnFinalScreen = true;
             }
@@ -1954,8 +1958,11 @@
                     minSpanY = item.minSpanY;
                 }
 
-                droppedOnOriginalCell = item.screenId == screenId && item.container == container
-                        && item.cellX == mTargetCell[0] && item.cellY == mTargetCell[1];
+                CellPos originalPresenterPos = getCellPosMapper().mapModelToPresenter(item);
+                droppedOnOriginalCell = originalPresenterPos.screenId == screenId
+                        && item.container == container
+                        && originalPresenterPos.cellX == mTargetCell[0]
+                        && originalPresenterPos.cellY == mTargetCell[1];
                 droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;
 
                 // When quickly moving an item, a user may accidentally rearrange their
@@ -3434,6 +3441,11 @@
                 > deviceProfile.availableWidthPx * SIGNIFICANT_MOVE_SCREEN_WIDTH_PERCENTAGE;
     }
 
+    @Override
+    public CellPosMapper getCellPosMapper() {
+        return mLauncher.getCellPosMapper();
+    }
+
     /**
      * Used as a workaround to ensure that the AppWidgetService receives the
      * PACKAGE_ADDED broadcast before updating widgets.
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index bf448c9..4768773 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -20,6 +20,8 @@
 import android.view.ViewGroup;
 
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.celllayout.CellPosMapper;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.ItemInfo;
@@ -48,15 +50,16 @@
      * See {@link #addInScreen}.
      */
     default void addInScreenFromBind(View child, ItemInfo info) {
-        int x = info.cellX;
-        int y = info.cellY;
+        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info);
+        int x = presenterPos.cellX;
+        int y = presenterPos.cellY;
         if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
                 || info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
-            int screenId = info.screenId;
+            int screenId = presenterPos.screenId;
             x = getHotseat().getCellXFromOrder(screenId);
             y = getHotseat().getCellYFromOrder(screenId);
         }
-        addInScreen(child, info.container, info.screenId, x, y, info.spanX, info.spanY);
+        addInScreen(child, info.container, presenterPos.screenId, x, y, info.spanX, info.spanY);
     }
 
     /**
@@ -64,7 +67,9 @@
      * See {@link #addInScreen(View, int, int, int, int, int, int)}.
      */
     default void addInScreen(View child, ItemInfo info) {
-        addInScreen(child, info.container, info.screenId, info.cellX, info.cellY,
+        CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info);
+        addInScreen(child, info.container,
+                presenterPos.screenId, presenterPos.cellX, presenterPos.cellY,
                 info.spanX, info.spanY);
     }
 
@@ -114,7 +119,7 @@
         ViewGroup.LayoutParams genericLp = child.getLayoutParams();
         CellLayoutLayoutParams lp;
         if (genericLp == null || !(genericLp instanceof CellLayoutLayoutParams)) {
-            lp = new CellLayoutLayoutParams(x, y, spanX, spanY, screenId);
+            lp = new CellLayoutLayoutParams(x, y, spanX, spanY);
         } else {
             lp = (CellLayoutLayoutParams) genericLp;
             lp.setCellX(x);
@@ -151,6 +156,11 @@
         return ItemLongClickListener.INSTANCE_WORKSPACE;
     }
 
+    /**
+     * Returns the mapper for converting between model and presenter
+     */
+    CellPosMapper getCellPosMapper();
+
     Hotseat getHotseat();
 
     CellLayout getScreenWithId(int screenId);
diff --git a/src/com/android/launcher3/celllayout/CellLayoutLayoutParams.java b/src/com/android/launcher3/celllayout/CellLayoutLayoutParams.java
index 726ef05..4b6a062 100644
--- a/src/com/android/launcher3/celllayout/CellLayoutLayoutParams.java
+++ b/src/com/android/launcher3/celllayout/CellLayoutLayoutParams.java
@@ -29,8 +29,6 @@
  */
 public class CellLayoutLayoutParams extends ViewGroup.MarginLayoutParams {
 
-    private int mScreenId = -1;
-
     @ViewDebug.ExportedProperty
     private int mCellX;
 
@@ -97,20 +95,17 @@
         this.mCellY = source.getCellY();
         this.cellHSpan = source.cellHSpan;
         this.cellVSpan = source.cellVSpan;
-        this.mScreenId = source.getScreenId();
         this.mTmpCellX = source.getTmpCellX();
         this.mTmpCellY = source.getTmpCellY();
         this.useTmpCoords = source.useTmpCoords;
     }
 
-    public CellLayoutLayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan,
-            int screenId) {
+    public CellLayoutLayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
         super(CellLayoutLayoutParams.MATCH_PARENT, CellLayoutLayoutParams.MATCH_PARENT);
         this.mCellX = cellX;
         this.mCellY = cellY;
         this.cellHSpan = cellHSpan;
         this.cellVSpan = cellVSpan;
-        this.mScreenId = screenId;
     }
 
     /**
@@ -178,14 +173,6 @@
         return "(" + this.getCellX() + ", " + this.getCellY() + ")";
     }
 
-    public int getScreenId() {
-        return mScreenId;
-    }
-
-    public void setScreenId(int screenId) {
-        this.mScreenId = screenId;
-    }
-
     /**
      * Horizontal location of the item in the grid.
      */
diff --git a/src/com/android/launcher3/celllayout/CellPosMapper.java b/src/com/android/launcher3/celllayout/CellPosMapper.java
new file mode 100644
index 0000000..1891696
--- /dev/null
+++ b/src/com/android/launcher3/celllayout/CellPosMapper.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.celllayout;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.Objects;
+
+/**
+ * Class for mapping between model position and presenter position.
+ */
+public class CellPosMapper {
+
+    public static final CellPosMapper DEFAULT = new CellPosMapper();
+
+    private CellPosMapper() { }
+
+    /**
+     * Maps the position in model to the position in view
+     */
+    public CellPos mapModelToPresenter(ItemInfo info) {
+        return new CellPos(info.cellX, info.cellY, info.screenId);
+    }
+
+    /**
+     * Maps the position in view to the position in model
+     */
+    public CellPos mapPresenterToModel(int presenterX, int presenterY, int presenterScreen,
+            int container) {
+        return new CellPos(presenterX, presenterY, presenterScreen);
+    }
+
+    /**
+     * Cell mapper which maps two panels into a single layout
+     */
+    public static class TwoPanelCellPosMapper extends CellPosMapper  {
+
+        private final int mColumnCount;
+
+        public TwoPanelCellPosMapper(int columnCount) {
+            mColumnCount = columnCount;
+        }
+
+        /**
+         * Maps the position in model to the position in view
+         */
+        public CellPos mapModelToPresenter(ItemInfo info) {
+            if (info.container != CONTAINER_DESKTOP || (info.screenId % 2) == 0) {
+                return super.mapModelToPresenter(info);
+            }
+            return new CellPos(info.cellX + mColumnCount, info.cellY, info.screenId - 1);
+        }
+
+        @Override
+        public CellPos mapPresenterToModel(int presenterX, int presenterY, int presenterScreen,
+                int container) {
+            if (container == CONTAINER_DESKTOP && (presenterScreen % 2) == 0
+                    && presenterX >= mColumnCount) {
+                return new CellPos(presenterX - mColumnCount, presenterY, presenterScreen + 1);
+            }
+            return super.mapPresenterToModel(presenterX, presenterY, presenterScreen, container);
+        }
+    }
+
+    /**
+     * Utility class to indicate the position of a cell
+     */
+    public static class CellPos {
+        public final int cellX;
+        public final int cellY;
+        public final int screenId;
+
+        public CellPos(int cellX, int cellY, int screenId) {
+            this.cellX = cellX;
+            this.cellY = cellY;
+            this.screenId = screenId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof CellPos)) return false;
+            CellPos cellPos = (CellPos) o;
+            return cellX == cellPos.cellX && cellY == cellPos.cellY && screenId == cellPos.screenId;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(cellX, cellY, screenId);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 10a2637..d43731b 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -222,7 +222,7 @@
         CellLayoutLayoutParams lp = (CellLayoutLayoutParams) textView.getLayoutParams();
         if (lp == null) {
             textView.setLayoutParams(new CellLayoutLayoutParams(
-                    item.cellX, item.cellY, item.spanX, item.spanY, item.screenId));
+                    item.cellX, item.cellY, item.spanX, item.spanY));
         } else {
             lp.setCellX(item.cellX);
             lp.setCellY(item.cellY);
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index 1f0a011..3e55425 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.logging.InstanceId;
 import com.android.launcher3.logging.StatsLogManager.StatsLogger;
@@ -177,7 +178,7 @@
         ModelWriter getModelWriter() {
             if (mWriter == null) {
                 mWriter = LauncherAppState.getInstance((Context) mContext).getModel()
-                        .getWriter(false, false, null);
+                        .getWriter(false, false, CellPosMapper.DEFAULT, null);
             }
             return mWriter;
         }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 4810b15..b061f8f 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -75,6 +75,7 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.WorkspaceLayoutManager;
 import com.android.launcher3.celllayout.CellLayoutLayoutParams;
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.icons.BaseIconFactory;
@@ -348,6 +349,11 @@
         return mWorkspaceScreens.get(screenId);
     }
 
+    @Override
+    public CellPosMapper getCellPosMapper() {
+        return CellPosMapper.DEFAULT;
+    }
+
     private void inflateAndAddIcon(WorkspaceItemInfo info) {
         CellLayout screen = mWorkspaceScreens.get(info.screenId);
         BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
@@ -539,10 +545,9 @@
         // Add first page QSB
         if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
             CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
-            View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen,
-                    false);
-            CellLayoutLayoutParams lp = new CellLayoutLayoutParams(0, 0, firstScreen.getCountX(),
-                    1, FIRST_SCREEN_ID);
+            View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
+            CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
+                    0, 0, firstScreen.getCountX(), 1);
             lp.canReorder = false;
             firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
         }
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 74a2c5d..01e58f2 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -24,6 +24,7 @@
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherModel.CallbackTask;
 import com.android.launcher3.LauncherModel.ModelUpdateTask;
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
 import com.android.launcher3.model.data.AppInfo;
@@ -96,7 +97,8 @@
     public ModelWriter getModelWriter() {
         // Updates from model task, do not deal with icon position in hotseat. Also no need to
         // verify changes as the ModelTasks always push the changes to callbacks
-        return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */, null);
+        return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */,
+                CellPosMapper.DEFAULT, null);
     }
 
     public void bindUpdatedWorkspaceItems(@NonNull final List<WorkspaceItemInfo> allUpdates) {
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index f444bd5..772ffa4 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -37,6 +37,8 @@
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.LauncherSettings.Settings;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.celllayout.CellPosMapper;
+import com.android.launcher3.celllayout.CellPosMapper.CellPos;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.model.BgDataModel.Callbacks;
@@ -81,9 +83,10 @@
     // Keep track of delete operations that occur when an Undo option is present; we may not commit.
     private final List<Runnable> mDeleteRunnables = new ArrayList<>();
     private boolean mPreparingToUndo;
+    private final CellPosMapper mCellPosMapper;
 
     public ModelWriter(Context context, LauncherModel model, BgDataModel dataModel,
-            boolean hasVerticalHotseat, boolean verifyChanges,
+            boolean hasVerticalHotseat, boolean verifyChanges, CellPosMapper cellPosMapper,
             @Nullable Callbacks owner) {
         mContext = context;
         mModel = model;
@@ -91,21 +94,24 @@
         mHasVerticalHotseat = hasVerticalHotseat;
         mVerifyChanges = verifyChanges;
         mOwner = owner;
+        mCellPosMapper = cellPosMapper;
         mUiExecutor = Executors.MAIN_EXECUTOR;
     }
 
     private void updateItemInfoProps(
             ItemInfo item, int container, int screenId, int cellX, int cellY) {
+        CellPos modelPos = mCellPosMapper.mapPresenterToModel(cellX, cellY, screenId, container);
+
         item.container = container;
-        item.cellX = cellX;
-        item.cellY = cellY;
+        item.cellX = modelPos.cellX;
+        item.cellY = modelPos.cellY;
         // We store hotseat items in canonical form which is this orientation invariant position
         // in the hotseat
         if (container == Favorites.CONTAINER_HOTSEAT) {
             item.screenId = mHasVerticalHotseat
                     ? LauncherAppState.getIDP(mContext).numDatabaseHotseatIcons - cellY - 1 : cellX;
         } else {
-            item.screenId = screenId;
+            item.screenId = modelPos.screenId;
         }
     }
 
diff --git a/src/com/android/launcher3/touch/ItemLongClickListener.java b/src/com/android/launcher3/touch/ItemLongClickListener.java
index 1421ece..7db7b0d 100644
--- a/src/com/android/launcher3/touch/ItemLongClickListener.java
+++ b/src/com/android/launcher3/touch/ItemLongClickListener.java
@@ -80,7 +80,8 @@
             }
         }
 
-        CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info);
+        CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info,
+                launcher.getCellPosMapper().mapModelToPresenter(info));
         launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
     }
 
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 79b4cb4..86028e7 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -54,6 +54,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
+import com.android.launcher3.celllayout.CellPosMapper;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.folder.FolderIcon;
@@ -427,6 +428,10 @@
         return false;
     }
 
+    default CellPosMapper getCellPosMapper() {
+        return CellPosMapper.DEFAULT;
+    }
+
     /**
      * Returns the ActivityContext associated with the given Context, or throws an exception if
      * the Context is not associated with any ActivityContext.