Robustly fixed issue where migration from different versions of DB data
tables was causing crashes upon grid size switching.

Bug: 304687723
Test: Verified behavior through unit test. Also manually tested that
factory resetting device -> adding shortcuts to the workspace ->
changing the grid size didn't cause any issues and resulted in normal
behavior.

Change-Id: I1945004d2c9a2d6f2b74d9597b39763ed4962e7b
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 105d5f3..34ebaf2 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -19,8 +19,13 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.provider.BaseColumns;
 
+import androidx.annotation.NonNull;
+
 import com.android.launcher3.model.data.ItemInfo;
 
+import java.util.LinkedHashMap;
+import java.util.stream.Collectors;
+
 /**
  * Settings related utilities.
  */
@@ -289,28 +294,51 @@
 
         public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
                 String tableName) {
-            String ifNotExists = optional ? " IF NOT EXISTS " : "";
-            db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
-                    "_id INTEGER PRIMARY KEY," +
-                    "title TEXT," +
-                    "intent TEXT," +
-                    "container INTEGER," +
-                    "screen INTEGER," +
-                    "cellX INTEGER," +
-                    "cellY INTEGER," +
-                    "spanX INTEGER," +
-                    "spanY INTEGER," +
-                    "itemType INTEGER," +
-                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
-                    "icon BLOB," +
-                    "appWidgetProvider TEXT," +
-                    "modified INTEGER NOT NULL DEFAULT 0," +
-                    "restored INTEGER NOT NULL DEFAULT 0," +
-                    "profileId INTEGER DEFAULT " + myProfileId + "," +
-                    "rank INTEGER NOT NULL DEFAULT 0," +
-                    "options INTEGER NOT NULL DEFAULT 0," +
-                    APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
-                    ");");
+            db.execSQL("CREATE TABLE " + (optional ? " IF NOT EXISTS " : "") + tableName + " ("
+                    + getJoinedColumnsToTypes(myProfileId) + ");");
+        }
+
+        // LinkedHashMap maintains Order of Insertion
+        @NonNull
+        private static LinkedHashMap<String, String> getColumnsToTypes(long profileId) {
+            final LinkedHashMap<String, String> columnsToTypes = new LinkedHashMap<>();
+            columnsToTypes.put(_ID, "INTEGER PRIMARY KEY");
+            columnsToTypes.put(TITLE, "TEXT");
+            columnsToTypes.put(INTENT, "TEXT");
+            columnsToTypes.put(CONTAINER, "INTEGER");
+            columnsToTypes.put(SCREEN, "INTEGER");
+            columnsToTypes.put(CELLX, "INTEGER");
+            columnsToTypes.put(CELLY, "INTEGER");
+            columnsToTypes.put(SPANX, "INTEGER");
+            columnsToTypes.put(SPANY, "INTEGER");
+            columnsToTypes.put(ITEM_TYPE, "INTEGER");
+            columnsToTypes.put(APPWIDGET_ID, "INTEGER NOT NULL DEFAULT -1");
+            columnsToTypes.put(ICON, "BLOB");
+            columnsToTypes.put(APPWIDGET_PROVIDER, "TEXT");
+            columnsToTypes.put(MODIFIED, "INTEGER NOT NULL DEFAULT 0");
+            columnsToTypes.put(RESTORED, "INTEGER NOT NULL DEFAULT 0");
+            columnsToTypes.put(PROFILE_ID, "INTEGER DEFAULT " + profileId);
+            columnsToTypes.put(RANK, "INTEGER NOT NULL DEFAULT 0");
+            columnsToTypes.put(OPTIONS, "INTEGER NOT NULL DEFAULT 0");
+            columnsToTypes.put(APPWIDGET_SOURCE, "INTEGER NOT NULL DEFAULT -1");
+            return columnsToTypes;
+        }
+
+        private static String getJoinedColumnsToTypes(long profileId) {
+            return getColumnsToTypes(profileId)
+                    .entrySet()
+                    .stream()
+                    .map(it -> it.getKey() + " " + it.getValue())
+                    .collect(Collectors.joining(", "));
+        }
+
+        /**
+         * Returns an ordered list of columns in the Favorites table as one string, ready to use in
+         * an SQL statement.
+         */
+        @NonNull
+        public static String getColumns(long profileId) {
+            return String.join(", ", getColumnsToTypes(profileId).keySet());
         }
     }
 
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index 575551b..30958d9 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.provider;
 
+import static com.android.launcher3.LauncherSettings.Favorites.getColumns;
 import static com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE;
 
 import android.content.ContentValues;
@@ -48,7 +49,6 @@
  * A set of utility methods for Launcher DB used for DB updates and migration.
  */
 public class LauncherDbUtils {
-
     /**
      * Returns a string which can be used as a where clause for DB query to match the given itemId
      */
@@ -90,10 +90,12 @@
         if (fromDb != toDb) {
             toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
             toDb.execSQL(
-                    "INSERT INTO " + toTable + " SELECT * FROM from_db." + fromTable);
+                    "INSERT INTO " + toTable + " SELECT " + getColumns(userSerial)
+                        + " FROM from_db." + fromTable);
             toDb.execSQL("DETACH DATABASE 'from_db'");
         } else {
-            toDb.execSQL("INSERT INTO " + toTable + " SELECT * FROM " + fromTable);
+            toDb.execSQL("INSERT INTO " + toTable + " SELECT " + getColumns(userSerial) + " FROM "
+                    + fromTable);
         }
     }