Merge "Fix widget dissapearing because of change of appWidgetId" into main
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index 299c952..f24a7c1 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.pm.InstallSessionHelper;
 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.ContentWriter;
 import com.android.launcher3.util.GridOccupancy;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
@@ -674,6 +675,11 @@
         private String mProvider;
         private Map<String, Set<Integer>> mFolderItems = new HashMap<>();
 
+        /**
+         * Id of the specific widget.
+         */
+        public int appWidgetId = NO_ID;
+
         /** Comparator according to the reading order */
         @Override
         public int compareTo(DbEntry another) {
@@ -707,6 +713,18 @@
             values.put(LauncherSettings.Favorites.SPANY, spanY);
         }
 
+        @Override
+        public void writeToValues(@NonNull ContentWriter writer) {
+            super.writeToValues(writer);
+            writer.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
+        }
+
+        @Override
+        public void readFromValues(@NonNull ContentValues values) {
+            super.readFromValues(values);
+            appWidgetId = values.getAsInteger(LauncherSettings.Favorites.APPWIDGET_ID);
+        }
+
         /** This id is not used in the DB is only used while doing the migration and it identifies
          * an entry on each workspace. For example two calculator icons would have the same
          * migration id even thought they have different database ids.
@@ -717,7 +735,10 @@
                 case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR:
                     return getFolderMigrationId();
                 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                    return mProvider;
+                    // mProvider is the app the widget belongs to and appWidgetId it's the unique
+                    // is of the widget, we need both because if you remove a widget and then add it
+                    // again, then it can change and the WidgetProvider would not know the widget.
+                    return mProvider + appWidgetId;
                 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                     final String intentStr = cleanIntentString(mIntent);
                     try {
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
index e773a86..a006fd7 100644
--- a/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
+++ b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
@@ -31,16 +31,17 @@
  *   account for cases where the user have the same item multiple times.
  */
 fun generateItemsForTest(
-    testCase: GridMigrationUnitTestCase,
-    repeatAfter: Int
+    boards: List<CellLayoutBoard>,
+    repeatAfterRange: Point
 ): List<WorkspaceItem> {
     val id = AtomicInteger(0)
     val widgetId = AtomicInteger(LauncherAppWidgetInfo.CUSTOM_WIDGET_ID - 1)
-    val boards = testCase.boards
     // Repeat the same appWidgetProvider and intent to have repeating widgets and icons and test
     // that case too
-    val getIntent = { i: Int -> "Intent ${i % repeatAfter}" }
-    val getProvider = { i: Int -> "com.test/test.Provider${i % repeatAfter}" }
+    val getIntent = { i: Int -> "Intent ${(i + repeatAfterRange.x) % repeatAfterRange.y}" }
+    val getProvider = { i: Int ->
+        "com.test/test.Provider${(i + repeatAfterRange.x) % repeatAfterRange.y }"
+    }
     val hotseatEntries =
         (0 until boards[0].width).map {
             WorkspaceItem(
@@ -97,11 +98,12 @@
                     container = LauncherSettings.Favorites.CONTAINER_DESKTOP
                 )
             }
-    return widgetEntries + hotseatEntries // + iconEntries
+    return widgetEntries + hotseatEntries + iconEntries
 }
 
 data class GridMigrationUnitTestCase(
     val boards: List<CellLayoutBoard>,
+    val destBoards: List<CellLayoutBoard>,
     val srcSize: Point,
     val targetSize: Point,
     val seed: Long
@@ -135,11 +137,27 @@
         return boards
     }
 
-    fun generateTestCase(): GridMigrationUnitTestCase {
-        var seed = generator.nextLong()
+    fun generateTestCase(isDestEmpty: Boolean): GridMigrationUnitTestCase {
+        val seed = generator.nextLong()
         val randomBoardGenerator = RandomBoardGenerator(Random(seed))
         val width = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
         val height = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+        val targetSize =
+            Point(
+                randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE),
+                randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+            )
+        val destBoards =
+            if (isDestEmpty) {
+                listOf()
+            } else {
+                generateBoards(
+                    boardGenerator = randomBoardGenerator,
+                    width = targetSize.x,
+                    height = targetSize.y,
+                    boardCount = randomBoardGenerator.getRandom(3, MAX_BOARD_COUNT)
+                )
+            }
         return GridMigrationUnitTestCase(
             boards =
                 generateBoards(
@@ -148,12 +166,9 @@
                     height = height,
                     boardCount = randomBoardGenerator.getRandom(3, MAX_BOARD_COUNT)
                 ),
+            destBoards = destBoards,
             srcSize = Point(width, height),
-            targetSize =
-                Point(
-                    randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE),
-                    randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
-                ),
+            targetSize = targetSize,
             seed = seed
         )
     }
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
index 1002976..58b915f 100644
--- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -40,14 +40,22 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+private data class Grid(val tableName: String, val size: Point, val items: List<WorkspaceItem>) {
+    fun toGridState(): DeviceGridState =
+        DeviceGridState(size.x, size.y, size.x, InvariantDeviceProfile.TYPE_PHONE, tableName)
+}
+
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class ValidGridMigrationUnitTest {
 
     companion object {
         const val SEED = 1044542
-        const val REPEAT_AFTER = 10
+        val REPEAT_AFTER = Point(0, 10)
+        val REPEAT_AFTER_DST = Point(6, 15)
         const val TAG = "ValidGridMigrationUnitTest"
+        const val SMALL_TEST_SIZE = 60
+        const val LARGE_TEST_SIZE = 1000
     }
 
     private lateinit var context: Context
@@ -57,14 +65,10 @@
         context = InstrumentationRegistry.getInstrumentation().targetContext
     }
 
-    private fun validate(
-        srcItems: List<WorkspaceItem>,
-        dstItems: List<WorkspaceItem>,
-        destinationSize: Point
-    ) {
+    private fun validate(srcGrid: Grid, dstGrid: Grid, resultItems: List<WorkspaceItem>) {
         // This returns a map with the number of repeated elements
         // ex { calculatorIcon : 6, weatherWidget : 2 }
-        val itemsToSet = { it: List<WorkspaceItem> ->
+        val itemsToMap = { it: List<WorkspaceItem> ->
             it.filter { it.container != Favorites.CONTAINER_HOTSEAT }
                 .groupingBy {
                     when (it.type) {
@@ -77,34 +81,36 @@
                 }
                 .eachCount()
         }
-        for (it in dstItems) {
-            assert((it.x in 0..destinationSize.x) && (it.y in 0..destinationSize.y)) {
-                "Item outside of the board size. Size = $destinationSize Item = $it"
+        resultItems.forEach {
+            assert((it.x in 0..dstGrid.size.x) && (it.y in 0..dstGrid.size.y)) {
+                "Item outside of the board size. Size = ${dstGrid.size} Item = $it"
             }
             assert(
-                (it.x + it.spanX in 0..destinationSize.x) &&
-                    (it.y + it.spanY in 0..destinationSize.y)
+                (it.x + it.spanX in 0..dstGrid.size.x) && (it.y + it.spanY in 0..dstGrid.size.y)
             ) {
-                "Item doesn't fit in the grid. Size = $destinationSize Item = $it"
+                "Item doesn't fit in the grid. Size = ${dstGrid.size} Item = $it"
             }
         }
 
-        assert(itemsToSet(srcItems) == itemsToSet(dstItems)) {
-            "The srcItems do not match the dstItems src = $srcItems  dst = $dstItems"
+        val srcCountMap = itemsToMap(srcGrid.items)
+        val resultCountMap = itemsToMap(resultItems)
+        val diff = resultCountMap - srcCountMap
+
+        diff.forEach { (k, count) ->
+            assert(count >= 0) { "Source item $k not present on the result" }
         }
     }
 
-    private fun addItemsToDb(db: SQLiteDatabase, tableName: String, items: List<WorkspaceItem>) {
+    private fun addItemsToDb(db: SQLiteDatabase, grid: Grid) {
         LauncherDbUtils.SQLiteTransaction(db).use { transaction ->
-            items.forEach { insertIntoDb(tableName, it, transaction.db) }
+            grid.items.forEach { insertIntoDb(grid.tableName, it, transaction.db) }
             transaction.commit()
         }
     }
 
     private fun migrate(
-        srcItems: List<WorkspaceItem>,
-        srcSize: Point,
-        targetSize: Point
+        srcGrid: Grid,
+        dstGrid: Grid,
     ): List<WorkspaceItem> {
         val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
         val dbHelper =
@@ -114,46 +120,64 @@
                 { UserCache.INSTANCE.get(context).getSerialNumberForUser(it) },
                 {}
             )
-        val srcTableName = Favorites.TMP_TABLE
-        val dstTableName = Favorites.TABLE_NAME
-        Favorites.addTableToDb(dbHelper.writableDatabase, userSerial, false, srcTableName)
-        addItemsToDb(dbHelper.writableDatabase, srcTableName, srcItems)
+
+        Favorites.addTableToDb(dbHelper.writableDatabase, userSerial, false, srcGrid.tableName)
+
+        addItemsToDb(dbHelper.writableDatabase, srcGrid)
+        addItemsToDb(dbHelper.writableDatabase, dstGrid)
+
         LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use {
             GridSizeMigrationUtil.migrate(
                 dbHelper,
-                GridSizeMigrationUtil.DbReader(it.db, srcTableName, context, MockSet(1)),
-                GridSizeMigrationUtil.DbReader(it.db, dstTableName, context, MockSet(1)),
-                targetSize.x,
-                targetSize,
-                DeviceGridState(
-                    srcSize.x,
-                    srcSize.y,
-                    srcSize.x,
-                    InvariantDeviceProfile.TYPE_PHONE,
-                    srcTableName
-                ),
-                DeviceGridState(
-                    targetSize.x,
-                    targetSize.y,
-                    targetSize.x,
-                    InvariantDeviceProfile.TYPE_PHONE,
-                    dstTableName
-                )
+                GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context, MockSet(1)),
+                GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context, MockSet(1)),
+                dstGrid.size.x,
+                dstGrid.size,
+                srcGrid.toGridState(),
+                dstGrid.toGridState()
             )
             it.commit()
         }
-        return readDb(dstTableName, dbHelper.readableDatabase)
+        return readDb(dstGrid.tableName, dbHelper.readableDatabase)
     }
 
     @Test
     fun runTestCase() {
         val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
-        for (i in 0..50) {
-            val testCase = caseGenerator.generateTestCase()
+        for (i in 0..SMALL_TEST_SIZE) {
+            val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
             Log.d(TAG, "Test case = $testCase")
-            val srcItemList = generateItemsForTest(testCase, REPEAT_AFTER)
-            val dstItemList = migrate(srcItemList, testCase.srcSize, testCase.targetSize)
-            validate(srcItemList, dstItemList, testCase.targetSize)
+            val srcGrid =
+                Grid(
+                    tableName = Favorites.TMP_TABLE,
+                    size = testCase.srcSize,
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                )
+            val dstGrid =
+                Grid(tableName = Favorites.TABLE_NAME, size = testCase.targetSize, items = listOf())
+            validate(srcGrid, dstGrid, migrate(srcGrid, dstGrid))
+        }
+    }
+
+    @Test
+    fun mergeBoards() {
+        val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
+        for (i in 0..SMALL_TEST_SIZE) {
+            val testCase = caseGenerator.generateTestCase(isDestEmpty = false)
+            Log.d(TAG, "Test case = $testCase")
+            val srcGrid =
+                Grid(
+                    tableName = Favorites.TMP_TABLE,
+                    size = testCase.srcSize,
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                )
+            val dstGrid =
+                Grid(
+                    tableName = Favorites.TABLE_NAME,
+                    size = testCase.targetSize,
+                    items = generateItemsForTest(testCase.destBoards, REPEAT_AFTER_DST)
+                )
+            validate(srcGrid, dstGrid, migrate(srcGrid, dstGrid))
         }
     }
 
@@ -162,12 +186,18 @@
     @Test
     fun runExtensiveTestCases() {
         val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
-        for (i in 0..1000) {
-            val testCase = caseGenerator.generateTestCase()
+        for (i in 0..LARGE_TEST_SIZE) {
+            val testCase = caseGenerator.generateTestCase(isDestEmpty = true)
             Log.d(TAG, "Test case = $testCase")
-            val srcItemList = generateItemsForTest(testCase, REPEAT_AFTER)
-            val dstItemList = migrate(srcItemList, testCase.srcSize, testCase.targetSize)
-            validate(srcItemList, dstItemList, testCase.targetSize)
+            val srcGrid =
+                Grid(
+                    tableName = Favorites.TMP_TABLE,
+                    size = testCase.srcSize,
+                    items = generateItemsForTest(testCase.boards, REPEAT_AFTER)
+                )
+            val dstGrid =
+                Grid(tableName = Favorites.TABLE_NAME, size = testCase.targetSize, items = listOf())
+            validate(srcGrid, dstGrid, migrate(srcGrid, dstGrid))
         }
     }
 }