The new grid migration algorithm

go/grid-migration-v2

When changing grid from option 1 to option 2, we calculate the diff and add the icons that are in option 1 but not option 2, to option 2's workspace, according to the reading order.

Test: manual and unit tests
Fixes: 144052802

Change-Id: Id01f69e90ce656a9b7c9051fed499807ee9ac0f7
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
new file mode 100644
index 0000000..8f58d8b
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model;
+
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_CONTENT_URI;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.os.Process;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.HashSet;
+
+/** Unit tests for {@link GridSizeMigrationTaskV2} */
+@RunWith(LauncherRoboTestRunner.class)
+public class GridSizeMigrationTaskV2Test {
+
+    private LauncherModelHelper mModelHelper;
+    private Context mContext;
+    private SQLiteDatabase mDb;
+
+    private HashSet<String> mValidPackages;
+    private InvariantDeviceProfile mIdp;
+
+    @Before
+    public void setUp() {
+        mModelHelper = new LauncherModelHelper();
+        mContext = RuntimeEnvironment.application;
+        mDb = mModelHelper.provider.getDb();
+
+        mValidPackages = new HashSet<>();
+        mValidPackages.add(TEST_PACKAGE);
+        mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+        long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+                Process.myUserHandle());
+        dropTable(mDb, LauncherSettings.Favorites.TMP_TABLE);
+        LauncherSettings.Favorites.addTableToDb(mDb, userSerial, false,
+                LauncherSettings.Favorites.TMP_TABLE);
+    }
+
+    @Test
+    public void testMigration() {
+        final String testPackage1 = "com.android.launcher3.validpackage1";
+        final String testPackage2 = "com.android.launcher3.validpackage2";
+        final String testPackage3 = "com.android.launcher3.validpackage3";
+        final String testPackage4 = "com.android.launcher3.validpackage4";
+        final String testPackage5 = "com.android.launcher3.validpackage5";
+        final String testPackage7 = "com.android.launcher3.validpackage7";
+
+        mValidPackages.add(testPackage1);
+        mValidPackages.add(testPackage2);
+        mValidPackages.add(testPackage3);
+        mValidPackages.add(testPackage4);
+        mValidPackages.add(testPackage5);
+        mValidPackages.add(testPackage7);
+
+        int[] srcHotseatItems = {
+                mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+                -1,
+                mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+                mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+        };
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage5, 5, TMP_CONTENT_URI);
+
+        int[] destHotseatItems = {
+                -1,
+                mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2),
+                -1,
+        };
+        mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
+
+        mIdp.numHotseatIcons = 3;
+        mIdp.numColumns = 3;
+        mIdp.numRows = 3;
+        GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+                LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages, 5);
+        GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+                LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages, 3);
+        GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+                destReader, 3, new Point(mIdp.numColumns, mIdp.numRows));
+        task.migrate();
+
+        Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+                new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+                "container=" + CONTAINER_HOTSEAT, null, null, null);
+        assertEquals(c.getCount(), 3);
+        int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+        int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 1);
+        assertTrue(c.getString(intentIndex).contains(testPackage2));
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 0);
+        assertTrue(c.getString(intentIndex).contains(testPackage1));
+        c.moveToNext();
+        assertEquals(c.getInt(screenIndex), 2);
+        assertTrue(c.getString(intentIndex).contains(testPackage3));
+        c.close();
+
+        c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+                new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY,
+                        LauncherSettings.Favorites.INTENT},
+                "container=" + CONTAINER_DESKTOP, null, null, null);
+        assertEquals(c.getCount(), 2);
+        intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+        int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX);
+        int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY);
+
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage7));
+        c.moveToNext();
+        assertTrue(c.getString(intentIndex).contains(testPackage5));
+        assertEquals(c.getInt(cellXIndex), 0);
+        assertEquals(c.getInt(cellYIndex), 2);
+    }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index e133cf2..20b1453 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -230,7 +230,21 @@
     }
 
     public int addItem(int type, int screen, int container, int x, int y) {
-        return addItem(type, screen, container, x, y, mDefaultProfileId);
+        return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, long profileId) {
+        return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, String packageName) {
+        return addItem(type, screen, container, x, y, mDefaultProfileId, packageName);
+    }
+
+    public int addItem(int type, int screen, int container, int x, int y, String packageName,
+            int id, Uri contentUri) {
+        addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri);
+        return id;
     }
 
     /**
@@ -238,11 +252,19 @@
      * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
      *             folder (where the type represents the number of items in the folder).
      */
-    public int addItem(int type, int screen, int container, int x, int y, long profileId) {
+    public int addItem(int type, int screen, int container, int x, int y, long profileId,
+            String packageName) {
         Context context = RuntimeEnvironment.application;
         int id = LauncherSettings.Settings.call(context.getContentResolver(),
                 LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
                 .getInt(LauncherSettings.Settings.EXTRA_VALUE);
+        addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI);
+        return id;
+    }
+
+    public void addItem(int type, int screen, int container, int x, int y, long profileId,
+            String packageName, int id, Uri contentUri) {
+        Context context = RuntimeEnvironment.application;
 
         ContentValues values = new ContentValues();
         values.put(LauncherSettings.Favorites._ID, id);
@@ -257,7 +279,7 @@
         if (type == APP_ICON || type == SHORTCUT) {
             values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
             values.put(LauncherSettings.Favorites.INTENT,
-                    new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
+                    new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
         } else {
             values.put(LauncherSettings.Favorites.ITEM_TYPE,
                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
@@ -267,8 +289,7 @@
             }
         }
 
-        context.getContentResolver().insert(CONTENT_URI, values);
-        return id;
+        context.getContentResolver().insert(contentUri, values);
     }
 
     public int[][][] createGrid(int[][][] typeArray) {
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 697048a..a699c32 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -17,6 +17,7 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -164,8 +165,11 @@
             return false;
         }
 
-        mOpenHelper.close();
+        DatabaseHelper oldHelper = mOpenHelper;
         mOpenHelper = new DatabaseHelper(getContext());
+        copyTable(oldHelper.getReadableDatabase(), Favorites.TABLE_NAME,
+                mOpenHelper.getWritableDatabase(), Favorites.TMP_TABLE, getContext());
+        oldHelper.close();
         return true;
     }
 
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 216c221..f516446 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -103,6 +103,11 @@
         public static final String PREVIEW_TABLE_NAME = "favorites_preview";
 
         /**
+         * Temporary table used specifically for multi-db grid migrations
+         */
+        public static final String TMP_TABLE = "favorites_tmp";
+
+        /**
          * The content:// style URL for "favorites" table
          */
         public static final Uri CONTENT_URI = Uri.parse("content://"
@@ -115,6 +120,12 @@
                 + LauncherProvider.AUTHORITY + "/" + PREVIEW_TABLE_NAME);
 
         /**
+         * The content:// style URL for "favorites_tmp" table
+         */
+        public static final Uri TMP_CONTENT_URI = Uri.parse("content://"
+                + LauncherProvider.AUTHORITY + "/" + TMP_TABLE);
+
+        /**
          * The content:// style URL for a given row, identified by its id.
          *
          * @param id The row id.
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index 3ba740d..a084600 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -936,8 +936,8 @@
 
             boolean dbChanged = false;
             if (migrateForPreview) {
-                copyTable(transaction.getDb(), Favorites.TABLE_NAME, Favorites.PREVIEW_TABLE_NAME,
-                        context);
+                copyTable(transaction.getDb(), Favorites.TABLE_NAME, transaction.getDb(),
+                        Favorites.PREVIEW_TABLE_NAME, context);
             }
 
             GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
@@ -950,10 +950,11 @@
 
             HashSet<String> validPackages = getValidPackages(context);
             // Hotseat.
-            if (srcHotseatCount != idp.numHotseatIcons) {
-                // Migrate hotseat.
-                dbChanged = new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
-                        migrateForPreview, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
+            if (srcHotseatCount != idp.numHotseatIcons
+                    && new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
+                            migrateForPreview, srcHotseatCount,
+                            idp.numHotseatIcons).migrateHotseat()) {
+                dbChanged = true;
             }
 
             // Grid size
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 197b29c..0bdccfa 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -16,9 +16,46 @@
 
 package com.android.launcher3.model;
 
+import static com.android.launcher3.Utilities.getPointString;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.WidgetManagerHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
@@ -26,14 +63,63 @@
  */
 public class GridSizeMigrationTaskV2 {
 
-    private GridSizeMigrationTaskV2(Context context) {
+    public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
+    public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
 
+    private static final String TAG = "GridSizeMigrationTaskV2";
+    private static final boolean DEBUG = true;
+
+    private final Context mContext;
+    private final SQLiteDatabase mDb;
+    private final DbReader mSrcReader;
+    private final DbReader mDestReader;
+
+    private final List<DbEntry> mHotseatItems;
+    private final List<DbEntry> mWorkspaceItems;
+
+    private final List<DbEntry> mHotseatDiff;
+    private final List<DbEntry> mWorkspaceDiff;
+
+    private final int mDestHotseatSize;
+    private final int mTrgX, mTrgY;
+
+    @VisibleForTesting
+    protected GridSizeMigrationTaskV2(Context context, SQLiteDatabase db, DbReader srcReader,
+            DbReader destReader, int destHotseatSize, Point targetSize) {
+        mContext = context;
+        mDb = db;
+        mSrcReader = srcReader;
+        mDestReader = destReader;
+
+        mHotseatItems = destReader.loadHotseatEntries();
+        mWorkspaceItems = destReader.loadAllWorkspaceEntries();
+
+        mHotseatDiff = calcDiff(mSrcReader.loadHotseatEntries(), mHotseatItems);
+        mWorkspaceDiff = calcDiff(mSrcReader.loadAllWorkspaceEntries(), mWorkspaceItems);
+        mDestHotseatSize = destHotseatSize;
+
+        mTrgX = targetSize.x;
+        mTrgY = targetSize.y;
+    }
+
+    /**
+     * Check given a new IDP, if migration is necessary.
+     */
+    public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+        return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+                || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+                idp.numHotseatIcons);
     }
 
     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
     public static boolean migrateGridIfNeeded(Context context) {
-        // To be implemented.
-        return true;
+        if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+            return true;
+        }
+        return migrateGridIfNeeded(context, null);
     }
 
     /**
@@ -43,7 +129,608 @@
      * @return false if the migration failed.
      */
     public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
-        // To be implemented.
+        boolean migrateForPreview = idp != null;
+        if (!migrateForPreview) {
+            idp = LauncherAppState.getIDP(context);
+        }
+
+        if (!needsToMigrate(context, idp)) {
+            return true;
+        }
+
+        SharedPreferences prefs = Utilities.getPrefs(context);
+        String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+        if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+                && idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+                idp.numHotseatIcons)) {
+            // Skip if workspace and hotseat sizes have not changed.
+            return true;
+        }
+
+        HashSet<String> validPackages = getValidPackages(context);
+        int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
+
+        if (!LauncherSettings.Settings.call(
+                context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER).getBoolean(
+                LauncherSettings.Settings.EXTRA_VALUE)) {
+            return false;
+        }
+
+        long migrationStartTime = System.currentTimeMillis();
+        try (SQLiteTransaction t = (SQLiteTransaction) LauncherSettings.Settings.call(
+                context.getContentResolver(),
+                LauncherSettings.Settings.METHOD_NEW_TRANSACTION).getBinder(
+                LauncherSettings.Settings.EXTRA_VALUE)) {
+
+            DbReader srcReader = new DbReader(t.getDb(), LauncherSettings.Favorites.TMP_TABLE,
+                    context, validPackages, srcHotseatCount);
+            DbReader destReader = new DbReader(t.getDb(), LauncherSettings.Favorites.TABLE_NAME,
+                    context, validPackages, idp.numHotseatIcons);
+
+            Point targetSize = new Point(idp.numColumns, idp.numRows);
+            GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(),
+                    srcReader, destReader, idp.numHotseatIcons, targetSize);
+            task.migrate();
+
+            dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
+
+            t.commit();
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, "Error during grid migration", e);
+
+            return false;
+        } finally {
+            Log.v(TAG, "Workspace migration completed in "
+                    + (System.currentTimeMillis() - migrationStartTime));
+
+            // Save current configuration, so that the migration does not run again.
+            prefs.edit()
+                    .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+                    .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+                    .apply();
+        }
+    }
+
+    @VisibleForTesting
+    protected boolean migrate() {
+        if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) {
+            return false;
+        }
+
+        // Migrate hotseat
+        HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader,
+                mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff);
+        hotseatSolution.find();
+
+        // Sort the items by the reading order.
+        Collections.sort(mWorkspaceDiff);
+
+        // Migrate workspace.
+        for (int screenId = 0; screenId <= mDestReader.mLastScreenId; screenId++) {
+            if (DEBUG) {
+                Log.d(TAG, "Migrating " + screenId);
+            }
+            List<DbEntry> entries = mDestReader.loadWorkspaceEntries(screenId);
+            GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+                    mContext, entries, screenId, mTrgX, mTrgY, mWorkspaceDiff);
+            workspaceSolution.find();
+            if (mWorkspaceDiff.isEmpty()) {
+                break;
+            }
+        }
+
+        int screenId = mDestReader.mLastScreenId + 1;
+        while (!mWorkspaceDiff.isEmpty()) {
+            GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+                    mContext, new ArrayList<>(), screenId, mTrgX, mTrgY, mWorkspaceDiff);
+            workspaceSolution.find();
+            screenId++;
+        }
         return true;
     }
+
+    /** Return what's in the src but not in the dest */
+    private static List<DbEntry> calcDiff(List<DbEntry> src, List<DbEntry> dest) {
+        Set<String> destSet = dest.parallelStream().map(DbEntry::getIntentStr).collect(
+                Collectors.toSet());
+        List<DbEntry> diff = new ArrayList<>();
+        for (DbEntry entry : src) {
+            if (!destSet.contains(entry.mIntent)) {
+                diff.add(entry);
+            }
+        }
+        return diff;
+    }
+
+    private static void insertEntryInDb(SQLiteDatabase db, Context context,
+            ArrayList<DbEntry> entriesFromSrcDb, DbEntry entry) {
+        int id = -1;
+        switch (entry.itemType) {
+            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+            case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+                for (DbEntry e : entriesFromSrcDb) {
+                    if (TextUtils.equals(e.mIntent, entry.mIntent)) {
+                        id = e.id;
+                    }
+                }
+
+                break;
+            }
+            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+                for (DbEntry e : entriesFromSrcDb) {
+                    if (e.mFolderItems.size() == entry.mFolderItems.size()
+                            && e.mFolderItems.containsAll(entry.mFolderItems)) {
+                        id = e.id;
+                    }
+                }
+                break;
+            }
+            case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+            case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
+                for (DbEntry e : entriesFromSrcDb) {
+                    if (TextUtils.equals(e.mProvider, entry.mProvider)) {
+                        id = e.id;
+                        break;
+                    }
+                }
+                break;
+            }
+            default:
+                return;
+        }
+
+        Cursor c = db.query(LauncherSettings.Favorites.TMP_TABLE, null,
+                LauncherSettings.Favorites._ID + " = '" + id + "'", null, null, null, null);
+
+        while (c.moveToNext()) {
+            ContentValues values = new ContentValues();
+            DatabaseUtils.cursorRowToContentValues(c, values);
+            entry.updateContentValues(values);
+            values.put(LauncherSettings.Favorites._ID,
+                    LauncherSettings.Settings.call(context.getContentResolver(),
+                            LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
+                            LauncherSettings.Settings.EXTRA_VALUE));
+            db.insert(LauncherSettings.Favorites.TABLE_NAME, null, values);
+        }
+        c.close();
+    }
+
+    private static void removeEntryFromDb(SQLiteDatabase db, IntArray entryId) {
+        db.delete(LauncherSettings.Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
+                LauncherSettings.Favorites._ID, entryId), null);
+    }
+
+    private static HashSet<String> getValidPackages(Context context) {
+        // Initialize list of valid packages. This contain all the packages which are already on
+        // the device and packages which are being installed. Any item which doesn't belong to
+        // this set is removed.
+        // Since the loader removes such items anyway, removing these items here doesn't cause
+        // any extra data loss and gives us more free space on the grid for better migration.
+        HashSet<String> validPackages = new HashSet<>();
+        for (PackageInfo info : context.getPackageManager()
+                .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
+            validPackages.add(info.packageName);
+        }
+        InstallSessionHelper.INSTANCE.get(context)
+                .getActiveSessions().keySet()
+                .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
+        return validPackages;
+    }
+
+    protected static class GridPlacementSolution {
+
+        private final SQLiteDatabase mDb;
+        private final DbReader mSrcReader;
+        private final Context mContext;
+        private final GridOccupancy mOccupied;
+        private final int mScreenId;
+        private final int mTrgX;
+        private final int mTrgY;
+        private final List<DbEntry> mItemsToPlace;
+
+        private int mNextStartX;
+        private int mNextStartY;
+
+        GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context,
+                List<DbEntry> placedWorkspaceItems, int screenId, int trgX,
+                int trgY, List<DbEntry> itemsToPlace) {
+            mDb = db;
+            mSrcReader = srcReader;
+            mContext = context;
+            mOccupied = new GridOccupancy(trgX, trgY);
+            mScreenId = screenId;
+            mTrgX = trgX;
+            mTrgY = trgY;
+            mNextStartX = 0;
+            mNextStartY = mTrgY - 1;
+            for (DbEntry entry : placedWorkspaceItems) {
+                mOccupied.markCells(entry, true);
+            }
+            mItemsToPlace = itemsToPlace;
+        }
+
+        public void find() {
+            Iterator<DbEntry> iterator = mItemsToPlace.iterator();
+            while (iterator.hasNext()) {
+                final DbEntry entry = iterator.next();
+                if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
+                    iterator.remove();
+                    continue;
+                }
+                if (findPlacement(entry)) {
+                    insertEntryInDb(mDb, mContext, mSrcReader.mWorkspaceEntries, entry);
+                    iterator.remove();
+                }
+            }
+        }
+
+        private boolean findPlacement(DbEntry entry) {
+            for (int y = mNextStartY; y >= 0; y--) {
+                for (int x = mNextStartX; x < mTrgX; x++) {
+                    boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
+                    boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX,
+                            entry.minSpanY);
+                    if (minFits) {
+                        entry.spanX = entry.minSpanX;
+                        entry.spanY = entry.minSpanY;
+                    }
+                    if (fits || minFits) {
+                        entry.screenId = mScreenId;
+                        entry.cellX = x;
+                        entry.cellY = y;
+                        mOccupied.markCells(entry, true);
+                        mNextStartX = x + entry.spanX;
+                        mNextStartY = y;
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+
+    protected static class HotseatPlacementSolution {
+
+        private final SQLiteDatabase mDb;
+        private final DbReader mSrcReader;
+        private final Context mContext;
+        private final HotseatOccupancy mOccupied;
+        private final List<DbEntry> mItemsToPlace;
+
+        HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context,
+                int hotseatSize, List<DbEntry> placedHotseatItems, List<DbEntry> itemsToPlace) {
+            mDb = db;
+            mSrcReader = srcReader;
+            mContext = context;
+            mOccupied = new HotseatOccupancy(hotseatSize);
+            for (DbEntry entry : placedHotseatItems) {
+                mOccupied.markCells(entry, true);
+            }
+            mItemsToPlace = itemsToPlace;
+        }
+
+        public void find() {
+            for (int i = 0; i < mOccupied.mCells.length; i++) {
+                if (!mOccupied.mCells[i] && !mItemsToPlace.isEmpty()) {
+                    DbEntry entry = mItemsToPlace.remove(0);
+                    entry.screenId = i;
+                    // These values does not affect the item position, but we should set them
+                    // to something other than -1.
+                    entry.cellX = i;
+                    entry.cellY = 0;
+                    insertEntryInDb(mDb, mContext, mSrcReader.mHotseatEntries, entry);
+                    mOccupied.markCells(entry, true);
+                }
+            }
+        }
+
+        private class HotseatOccupancy {
+
+            private final boolean[] mCells;
+
+            private HotseatOccupancy(int hotseatSize) {
+                mCells = new boolean[hotseatSize];
+            }
+
+            private void markCells(ItemInfo item, boolean value) {
+                mCells[item.screenId] = value;
+            }
+        }
+    }
+
+    protected static class DbReader {
+
+        private final SQLiteDatabase mDb;
+        private final String mTableName;
+        private final Context mContext;
+        private final HashSet<String> mValidPackages;
+        private final int mHotseatSize;
+        private int mLastScreenId = -1;
+
+        private final ArrayList<DbEntry> mHotseatEntries = new ArrayList<>();
+        private final ArrayList<DbEntry> mWorkspaceEntries = new ArrayList<>();
+
+        DbReader(SQLiteDatabase db, String tableName, Context context,
+                HashSet<String> validPackages, int hotseatSize) {
+            mDb = db;
+            mTableName = tableName;
+            mContext = context;
+            mValidPackages = validPackages;
+            mHotseatSize = hotseatSize;
+        }
+
+        protected ArrayList<DbEntry> loadHotseatEntries() {
+            Cursor c = queryWorkspace(
+                    new String[]{
+                            LauncherSettings.Favorites._ID,                  // 0
+                            LauncherSettings.Favorites.ITEM_TYPE,            // 1
+                            LauncherSettings.Favorites.INTENT,               // 2
+                            LauncherSettings.Favorites.SCREEN},              // 3
+                    LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+
+            final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+            final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+            final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+            final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+
+            IntArray entriesToRemove = new IntArray();
+            while (c.moveToNext()) {
+                DbEntry entry = new DbEntry();
+                entry.id = c.getInt(indexId);
+                entry.itemType = c.getInt(indexItemType);
+                entry.screenId = c.getInt(indexScreen);
+
+                if (entry.screenId >= mHotseatSize) {
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+
+                try {
+                    // calculate weight
+                    switch (entry.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+                            entry.mIntent = c.getString(indexIntent);
+                            verifyIntent(c.getString(indexIntent));
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+                            int total = getFolderItemsCount(entry);
+                            if (total == 0) {
+                                throw new Exception("Folder is empty");
+                            }
+                            break;
+                        }
+                        default:
+                            throw new Exception("Invalid item type");
+                    }
+                } catch (Exception e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Removing item " + entry.id, e);
+                    }
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+                mHotseatEntries.add(entry);
+            }
+            removeEntryFromDb(mDb, entriesToRemove);
+            c.close();
+            return mHotseatEntries;
+        }
+
+        protected ArrayList<DbEntry> loadAllWorkspaceEntries() {
+            Cursor c = queryWorkspace(
+                    new String[]{
+                            LauncherSettings.Favorites._ID,                  // 0
+                            LauncherSettings.Favorites.ITEM_TYPE,            // 1
+                            LauncherSettings.Favorites.SCREEN,               // 2
+                            LauncherSettings.Favorites.CELLX,                // 3
+                            LauncherSettings.Favorites.CELLY,                // 4
+                            LauncherSettings.Favorites.SPANX,                // 5
+                            LauncherSettings.Favorites.SPANY,                // 6
+                            LauncherSettings.Favorites.INTENT,               // 7
+                            LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
+                            LauncherSettings.Favorites.APPWIDGET_ID},        // 9
+                        LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_DESKTOP);
+            return loadWorkspaceEntries(c);
+        }
+
+        protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
+            Cursor c = queryWorkspace(
+                    new String[]{
+                            LauncherSettings.Favorites._ID,                  // 0
+                            LauncherSettings.Favorites.ITEM_TYPE,            // 1
+                            LauncherSettings.Favorites.SCREEN,               // 2
+                            LauncherSettings.Favorites.CELLX,                // 3
+                            LauncherSettings.Favorites.CELLY,                // 4
+                            LauncherSettings.Favorites.SPANX,                // 5
+                            LauncherSettings.Favorites.SPANY,                // 6
+                            LauncherSettings.Favorites.INTENT,               // 7
+                            LauncherSettings.Favorites.APPWIDGET_PROVIDER,   // 8
+                            LauncherSettings.Favorites.APPWIDGET_ID},        // 9
+                    LauncherSettings.Favorites.CONTAINER + " = "
+                            + LauncherSettings.Favorites.CONTAINER_DESKTOP
+                            + " AND " + LauncherSettings.Favorites.SCREEN + " = " + screen);
+            return loadWorkspaceEntries(c);
+        }
+
+        private ArrayList<DbEntry> loadWorkspaceEntries(Cursor c) {
+            final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+            final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+            final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+            final int indexCellX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
+            final int indexCellY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+            final int indexSpanX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
+            final int indexSpanY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
+            final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+            final int indexAppWidgetProvider = c.getColumnIndexOrThrow(
+                    LauncherSettings.Favorites.APPWIDGET_PROVIDER);
+            final int indexAppWidgetId = c.getColumnIndexOrThrow(
+                    LauncherSettings.Favorites.APPWIDGET_ID);
+
+            IntArray entriesToRemove = new IntArray();
+            WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
+            while (c.moveToNext()) {
+                DbEntry entry = new DbEntry();
+                entry.id = c.getInt(indexId);
+                entry.itemType = c.getInt(indexItemType);
+                entry.screenId = c.getInt(indexScreen);
+                mLastScreenId = Math.max(mLastScreenId, entry.screenId);
+                entry.cellX = c.getInt(indexCellX);
+                entry.cellY = c.getInt(indexCellY);
+                entry.spanX = c.getInt(indexSpanX);
+                entry.spanY = c.getInt(indexSpanY);
+
+                try {
+                    // calculate weight
+                    switch (entry.itemType) {
+                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+                            entry.mIntent = c.getString(indexIntent);
+                            verifyIntent(entry.mIntent);
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
+                            entry.mProvider = c.getString(indexAppWidgetProvider);
+                            ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
+                            verifyPackage(cn.getPackageName());
+
+                            int widgetId = c.getInt(indexAppWidgetId);
+                            LauncherAppWidgetProviderInfo pInfo =
+                                    widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
+                            Point spans = null;
+                            if (pInfo != null) {
+                                spans = pInfo.getMinSpans();
+                            }
+                            if (spans != null) {
+                                entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
+                                entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
+                            } else {
+                                // Assume that the widget be resized down to 2x2
+                                entry.minSpanX = entry.minSpanY = 2;
+                            }
+
+                            break;
+                        }
+                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+                            int total = getFolderItemsCount(entry);
+                            if (total == 0) {
+                                throw new Exception("Folder is empty");
+                            }
+                            break;
+                        }
+                        default:
+                            throw new Exception("Invalid item type");
+                    }
+                } catch (Exception e) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Removing item " + entry.id, e);
+                    }
+                    entriesToRemove.add(entry.id);
+                    continue;
+                }
+                mWorkspaceEntries.add(entry);
+            }
+            removeEntryFromDb(mDb, entriesToRemove);
+            c.close();
+            return mWorkspaceEntries;
+        }
+
+        private int getFolderItemsCount(DbEntry entry) {
+            Cursor c = queryWorkspace(
+                    new String[]{LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT},
+                    LauncherSettings.Favorites.CONTAINER + " = " + entry.id);
+
+            int total = 0;
+            while (c.moveToNext()) {
+                try {
+                    String intent = c.getString(1);
+                    verifyIntent(intent);
+                    total++;
+                    entry.mFolderItems.add(intent);
+                } catch (Exception e) {
+                    removeEntryFromDb(mDb, IntArray.wrap(c.getInt(0)));
+                }
+            }
+            c.close();
+            return total;
+        }
+
+        private Cursor queryWorkspace(String[] columns, String where) {
+            return mDb.query(mTableName, columns, where, null, null, null, null);
+        }
+
+        /** Verifies if the mIntent should be restored. */
+        private void verifyIntent(String intentStr)
+                throws Exception {
+            Intent intent = Intent.parseUri(intentStr, 0);
+            if (intent.getComponent() != null) {
+                verifyPackage(intent.getComponent().getPackageName());
+            } else if (intent.getPackage() != null) {
+                // Only verify package if the component was null.
+                verifyPackage(intent.getPackage());
+            }
+        }
+
+        /** Verifies if the package should be restored */
+        private void verifyPackage(String packageName)
+                throws Exception {
+            if (!mValidPackages.contains(packageName)) {
+                // TODO(b/151468819): Handle promise app icon restoration during grid migration.
+                throw new Exception("Package not available");
+            }
+        }
+    }
+
+    protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
+
+        private String mIntent;
+        private String mProvider;
+        private Set<String> mFolderItems = new HashSet<>();
+
+        /** Comparator according to the reading order */
+        @Override
+        public int compareTo(DbEntry another) {
+            if (screenId != another.screenId) {
+                return Integer.compare(screenId, another.screenId);
+            }
+            if (cellY != another.cellY) {
+                return -Integer.compare(cellY, another.cellY);
+            }
+            return Integer.compare(cellX, another.cellX);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            DbEntry entry = (DbEntry) o;
+            return Objects.equals(mIntent, entry.mIntent);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mIntent);
+        }
+
+        public void updateContentValues(ContentValues values) {
+            values.put(LauncherSettings.Favorites.SCREEN, screenId);
+            values.put(LauncherSettings.Favorites.CELLX, cellX);
+            values.put(LauncherSettings.Favorites.CELLY, cellY);
+            values.put(LauncherSettings.Favorites.SPANX, spanX);
+            values.put(LauncherSettings.Favorites.SPANY, spanY);
+        }
+
+        public String getIntentStr() {
+            return mIntent;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index f7ecc3f..dacea84 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -118,13 +118,20 @@
         db.execSQL("DROP TABLE IF EXISTS " + tableName);
     }
 
-    /** Copy from table to the to table. */
-    public static void copyTable(SQLiteDatabase db, String from, String to, Context context) {
+    /** Copy fromTable in fromDb to toTable in toDb. */
+    public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
+            String toTable, Context context) {
         long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
                 Process.myUserHandle());
-        dropTable(db, to);
-        Favorites.addTableToDb(db, userSerial, false, to);
-        db.execSQL("INSERT INTO " + to + " SELECT * FROM " + from);
+        dropTable(toDb, toTable);
+        Favorites.addTableToDb(toDb, userSerial, false, toTable);
+        if (fromDb != toDb) {
+            toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
+            toDb.execSQL(
+                    "INSERT INTO " + toTable + " SELECT * FROM from_db." + fromTable);
+        } else {
+            toDb.execSQL("INSERT INTO " + toTable + " SELECT * FROM " + fromTable);
+        }
     }
 
     /**