Generate grid migration cases and test if they are valid
This makes sure the grid migration logic always produces
valid results.
Bug: 313900847
Bug: 323525592
Flag: NA
Test: ValidGridMigrationUnitTest.kt
Change-Id: I76b19e1fa315f8a997afad34e5a4df7cc465b0c2
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 6d64c22..1c34c72 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -397,11 +397,6 @@
+ "waiting for SystemUI and then merging the SystemUI progress whenever we "
+ "start receiving the events");
- // TODO(Block 24): Clean up flags
- public static final BooleanFlag ENABLE_NEW_MIGRATION_LOGIC = getDebugFlag(270393455,
- "ENABLE_NEW_MIGRATION_LOGIC", ENABLED,
- "Enable the new grid migration logic, keeping pages when src < dest");
-
// TODO(Block 25): Clean up flags
public static final BooleanFlag ENABLE_NEW_GESTURE_NAV_TUTORIAL = getDebugFlag(270396257,
"ENABLE_NEW_GESTURE_NAV_TUTORIAL", ENABLED,
diff --git a/src/com/android/launcher3/model/DeviceGridState.java b/src/com/android/launcher3/model/DeviceGridState.java
index 8c68eb8..729b381 100644
--- a/src/com/android/launcher3/model/DeviceGridState.java
+++ b/src/com/android/launcher3/model/DeviceGridState.java
@@ -156,11 +156,11 @@
}
public Integer getColumns() {
- return Integer.parseInt(String.valueOf(mGridSizeString.charAt(0)));
+ return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[0]));
}
public Integer getRows() {
- return Integer.parseInt(String.valueOf(mGridSizeString.charAt(2)));
+ return Integer.parseInt(String.valueOf(mGridSizeString.split(",")[1]));
}
@Override
diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
index 15190c7..299c952 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationUtil.java
@@ -223,19 +223,13 @@
screens.add(screenId);
}
- boolean preservePages = false;
- if (screens.isEmpty() && FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC.get()) {
- preservePages = destDeviceState.compareTo(srcDeviceState) >= 0
- && destDeviceState.getColumns() - srcDeviceState.getColumns() <= 2;
- }
-
// Then we place the items on the screens
for (int screenId : screens) {
if (DEBUG) {
Log.d(TAG, "Migrating " + screenId);
}
solveGridPlacement(helper, srcReader,
- destReader, screenId, trgX, trgY, workspaceToBeAdded, false);
+ destReader, screenId, trgX, trgY, workspaceToBeAdded);
if (workspaceToBeAdded.isEmpty()) {
break;
}
@@ -245,8 +239,8 @@
// any of the screens, in this case we add them to new screens until all of them are placed.
int screenId = destReader.mLastScreenId + 1;
while (!workspaceToBeAdded.isEmpty()) {
- solveGridPlacement(helper, srcReader,
- destReader, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
+ solveGridPlacement(helper, srcReader, destReader, screenId, trgX, trgY,
+ workspaceToBeAdded);
screenId++;
}
@@ -348,7 +342,7 @@
private static void solveGridPlacement(@NonNull final DatabaseHelper helper,
@NonNull final DbReader srcReader, @NonNull final DbReader destReader,
final int screenId, final int trgX, final int trgY,
- @NonNull final List<DbEntry> sortedItemsToPlace, final boolean matchingScreenIdOnly) {
+ @NonNull final List<DbEntry> sortedItemsToPlace) {
final GridOccupancy occupied = new GridOccupancy(trgX, trgY);
final Point trg = new Point(trgX, trgY);
final Point next = new Point(0, screenId == 0
@@ -366,8 +360,6 @@
Iterator<DbEntry> iterator = sortedItemsToPlace.iterator();
while (iterator.hasNext()) {
final DbEntry entry = iterator.next();
- if (matchingScreenIdOnly && entry.screenId < screenId) continue;
- if (matchingScreenIdOnly && entry.screenId > screenId) break;
if (entry.minSpanX > trgX || entry.minSpanY > trgY) {
iterator.remove();
continue;
@@ -435,7 +427,8 @@
}
}
- protected static class DbReader {
+ @VisibleForTesting
+ public static class DbReader {
private final SQLiteDatabase mDb;
private final String mTableName;
@@ -446,7 +439,7 @@
private final Map<Integer, ArrayList<DbEntry>> mWorkspaceEntriesByScreenId =
new ArrayMap<>();
- DbReader(SQLiteDatabase db, String tableName, Context context,
+ public DbReader(SQLiteDatabase db, String tableName, Context context,
Set<String> validPackages) {
mDb = db;
mTableName = tableName;
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
index c5dbce4..ff46987 100644
--- a/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
+++ b/tests/src/com/android/launcher3/celllayout/testgenerator/RandomBoardGenerator.kt
@@ -21,6 +21,13 @@
/** Generates a random CellLayoutBoard. */
open class RandomBoardGenerator(generator: Random) : DeterministicRandomGenerator(generator) {
+
+ companion object {
+ // This is the max number of widgets because we encode the widgets as letters A-Z and we
+ // already have some of those letter used by other things so 22 is a safe number
+ val MAX_NUMBER_OF_WIDGETS = 22
+ }
+
/**
* @param remainingEmptySpaces the maximum number of spaces we will fill with icons and widgets
* meaning that if the number is 100 we will try to fill the board with at most 100 spaces
@@ -33,9 +40,9 @@
}
protected fun fillBoard(
- board: CellLayoutBoard,
- area: Rect,
- remainingEmptySpacesArg: Int
+ board: CellLayoutBoard,
+ area: Rect,
+ remainingEmptySpacesArg: Int
): CellLayoutBoard {
var remainingEmptySpaces = remainingEmptySpacesArg
if (area.height() * area.width() <= 0) return board
@@ -45,11 +52,18 @@
val y = area.top + getRandom(0, area.height() - height)
if (remainingEmptySpaces > 0) {
remainingEmptySpaces -= width * height
- } else if (board.widgets.size <= 22 && width * height > 1) {
+ }
+
+ if (board.widgets.size <= MAX_NUMBER_OF_WIDGETS && width * height > 1) {
board.addWidget(x, y, width, height)
} else {
board.addIcon(x, y)
}
+
+ if (remainingEmptySpaces < 0) {
+ // optimization, no need to keep going
+ return board
+ }
fillBoard(board, Rect(area.left, area.top, area.right, y), remainingEmptySpaces)
fillBoard(board, Rect(area.left, y, x, area.bottom), remainingEmptySpaces)
fillBoard(board, Rect(x, y + height, area.right, area.bottom), remainingEmptySpaces)
diff --git a/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
new file mode 100644
index 0000000..e773a86
--- /dev/null
+++ b/tests/src/com/android/launcher3/celllayout/testgenerator/ValidGridMigrationTestCaseGenerator.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.celllayout.testgenerator
+
+import android.graphics.Point
+import com.android.launcher3.LauncherSettings
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.gridmigration.WorkspaceItem
+import java.util.Random
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Generate a list of WorkspaceItem's for the given test case.
+ *
+ * @param repeatAfter a number after which we would repeat the same number of icons and widgets to
+ * account for cases where the user have the same item multiple times.
+ */
+fun generateItemsForTest(
+ testCase: GridMigrationUnitTestCase,
+ repeatAfter: Int
+): 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 hotseatEntries =
+ (0 until boards[0].width).map {
+ WorkspaceItem(
+ x = it,
+ y = 0,
+ spanX = 1,
+ spanY = 1,
+ id = id.getAndAdd(1),
+ screenId = it,
+ title = "Hotseat ${id.get()}",
+ appWidgetId = -1,
+ appWidgetProvider = "Hotseat icons don't have a provider",
+ intent = getIntent(id.get()),
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION,
+ container = LauncherSettings.Favorites.CONTAINER_HOTSEAT
+ )
+ }
+ var widgetEntries =
+ boards
+ .flatMapIndexed { i, board -> board.widgets.map { Pair(i, it) } }
+ .map {
+ WorkspaceItem(
+ x = it.second.cellX,
+ y = it.second.cellY,
+ spanX = it.second.spanX,
+ spanY = it.second.spanY,
+ id = id.getAndAdd(1),
+ screenId = it.first,
+ title = "Title Widget ${id.get()}",
+ appWidgetId = widgetId.getAndAdd(-1),
+ appWidgetProvider = getProvider(id.get()),
+ intent = "Widgets don't have intent",
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET,
+ container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+ )
+ }
+ widgetEntries = widgetEntries.filter { it.appWidgetProvider.contains("Provider4") }
+ val iconEntries =
+ boards
+ .flatMapIndexed { i, board -> board.icons.map { Pair(i, it) } }
+ .map {
+ WorkspaceItem(
+ x = it.second.coord.x,
+ y = it.second.coord.y,
+ spanX = 1,
+ spanY = 1,
+ id = id.getAndAdd(1),
+ screenId = it.first,
+ title = "Title Icon ${id.get()}",
+ appWidgetId = -1,
+ appWidgetProvider = "Icons don't have providers",
+ intent = getIntent(id.get()),
+ type = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION,
+ container = LauncherSettings.Favorites.CONTAINER_DESKTOP
+ )
+ }
+ return widgetEntries + hotseatEntries // + iconEntries
+}
+
+data class GridMigrationUnitTestCase(
+ val boards: List<CellLayoutBoard>,
+ val srcSize: Point,
+ val targetSize: Point,
+ val seed: Long
+)
+
+class ValidGridMigrationTestCaseGenerator(private val generator: Random) :
+ DeterministicRandomGenerator(generator) {
+
+ companion object {
+ const val MAX_BOARD_SIZE = 12
+ const val MAX_BOARD_COUNT = 10
+ const val SEED = 10342
+ }
+
+ private fun generateBoards(
+ boardGenerator: RandomBoardGenerator,
+ width: Int,
+ height: Int,
+ boardCount: Int
+ ): List<CellLayoutBoard> {
+ val boards = mutableListOf<CellLayoutBoard>()
+ for (i in 0 until boardCount) {
+ boards.add(
+ boardGenerator.generateBoard(
+ width,
+ height,
+ boardGenerator.getRandom(0, width * height)
+ )
+ )
+ }
+ return boards
+ }
+
+ fun generateTestCase(): GridMigrationUnitTestCase {
+ var seed = generator.nextLong()
+ val randomBoardGenerator = RandomBoardGenerator(Random(seed))
+ val width = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ val height = randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ return GridMigrationUnitTestCase(
+ boards =
+ generateBoards(
+ boardGenerator = randomBoardGenerator,
+ width = width,
+ height = height,
+ boardCount = randomBoardGenerator.getRandom(3, MAX_BOARD_COUNT)
+ ),
+ srcSize = Point(width, height),
+ targetSize =
+ Point(
+ randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE),
+ randomBoardGenerator.getRandom(3, MAX_BOARD_SIZE)
+ ),
+ seed = seed
+ )
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
index 04735f2..761f06d 100644
--- a/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
+++ b/tests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt
@@ -28,7 +28,6 @@
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE
import com.android.launcher3.LauncherSettings.Favorites.*
-import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.model.GridSizeMigrationUtil.DbReader
import com.android.launcher3.pm.UserCache
import com.android.launcher3.provider.LauncherDbUtils
@@ -98,10 +97,7 @@
modelHelper.destroy()
}
- /**
- * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is not
- * needed anymore
- */
+ /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
fun testMigration() {
@@ -208,10 +204,7 @@
assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
}
- /**
- * Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is not
- * needed anymore
- */
+ /** Old migration logic, should be modified once is not needed anymore */
@Test
@Throws(Exception::class)
fun testMigrationBackAndForth() {
@@ -606,68 +599,6 @@
}
/**
- * Migrating from a smaller grid to a large one should keep the pages if the column difference
- * is less than 2
- */
- @Test
- @Throws(Exception::class)
- fun migrateFromSmallerGridSmallDifference() {
- enableNewMigrationLogic("4,4")
-
- // Setup src grid
- addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 2, testPackage1, 5, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 2, 3, testPackage2, 6, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 1, testPackage3, 7, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 1, CONTAINER_DESKTOP, 3, 2, testPackage4, 8, TMP_TABLE)
- addItem(ITEM_TYPE_APPLICATION, 2, CONTAINER_DESKTOP, 3, 3, testPackage5, 9, TMP_TABLE)
-
- idp.numDatabaseHotseatIcons = 4
- idp.numColumns = 6
- idp.numRows = 5
-
- val srcReader = DbReader(db, TMP_TABLE, context, validPackages)
- val destReader = DbReader(db, TABLE_NAME, context, validPackages)
- GridSizeMigrationUtil.migrate(
- dbHelper,
- srcReader,
- destReader,
- idp.numDatabaseHotseatIcons,
- Point(idp.numColumns, idp.numRows),
- DeviceGridState(context),
- DeviceGridState(idp)
- )
-
- // Get workspace items
- val c =
- db.query(
- TABLE_NAME,
- arrayOf(INTENT, SCREEN),
- "container=$CONTAINER_DESKTOP",
- null,
- null,
- null,
- null
- )
- ?: throw IllegalStateException()
- val intentIndex = c.getColumnIndex(INTENT)
- val screenIndex = c.getColumnIndex(SCREEN)
-
- // Get in which screen the icon is
- val locMap = HashMap<String?, Int>()
- while (c.moveToNext()) {
- locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] =
- c.getInt(screenIndex)
- }
- c.close()
- assertThat(locMap.size).isEqualTo(5)
- assertThat(locMap[testPackage1]).isEqualTo(0)
- assertThat(locMap[testPackage2]).isEqualTo(0)
- assertThat(locMap[testPackage3]).isEqualTo(1)
- assertThat(locMap[testPackage4]).isEqualTo(1)
- assertThat(locMap[testPackage5]).isEqualTo(2)
- }
-
- /**
* Migrating from a smaller grid to a large one should reflow the pages if the column difference
* is more than 2
*/
diff --git a/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt b/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt
new file mode 100644
index 0000000..cc8e61d
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/gridmigration/GridMigrationUtils.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model.gridmigration
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.celllayout.board.CellLayoutBoard
+
+class MockSet(override val size: Int) : Set<String> {
+ override fun contains(element: String): Boolean = true
+ override fun containsAll(elements: Collection<String>): Boolean = true
+ override fun isEmpty(): Boolean = false
+ override fun iterator(): Iterator<String> = listOf<String>().iterator()
+}
+
+fun itemListToBoard(itemsArg: List<WorkspaceItem>, boardSize: Point): List<CellLayoutBoard> {
+ val items = itemsArg.filter { it.container != Favorites.CONTAINER_HOTSEAT }
+ val boardList =
+ List(items.maxOf { it.screenId + 1 }) { CellLayoutBoard(boardSize.x, boardSize.y) }
+ items.forEach {
+ when (it.type) {
+ Favorites.ITEM_TYPE_FOLDER,
+ Favorites.ITEM_TYPE_APP_PAIR -> throw Exception("Not implemented")
+ Favorites.ITEM_TYPE_APPWIDGET ->
+ boardList[it.screenId].addWidget(it.x, it.y, it.spanX, it.spanY)
+ Favorites.ITEM_TYPE_APPLICATION -> boardList[it.screenId].addIcon(it.x, it.y)
+ }
+ }
+ return boardList
+}
+
+fun insertIntoDb(tableName: String, entry: WorkspaceItem, db: SQLiteDatabase) {
+ val values = ContentValues()
+ values.put(Favorites.SCREEN, entry.screenId)
+ values.put(Favorites.CELLX, entry.x)
+ values.put(Favorites.CELLY, entry.y)
+ values.put(Favorites.SPANX, entry.spanX)
+ values.put(Favorites.SPANY, entry.spanY)
+ values.put(Favorites.TITLE, entry.title)
+ values.put(Favorites.INTENT, entry.intent)
+ values.put(Favorites.APPWIDGET_PROVIDER, entry.appWidgetProvider)
+ values.put(Favorites.APPWIDGET_ID, entry.appWidgetId)
+ values.put(Favorites.CONTAINER, entry.container)
+ values.put(Favorites.ITEM_TYPE, entry.type)
+ values.put(Favorites._ID, entry.id)
+ db.insert(tableName, null, values)
+}
+
+fun readDb(tableName: String, db: SQLiteDatabase): List<WorkspaceItem> {
+ val result = mutableListOf<WorkspaceItem>()
+ val cursor = db.query(tableName, null, null, null, null, null, null)
+ val indexCellX: Int = cursor.getColumnIndexOrThrow(Favorites.CELLX)
+ val indexCellY: Int = cursor.getColumnIndexOrThrow(Favorites.CELLY)
+ val indexSpanX: Int = cursor.getColumnIndexOrThrow(Favorites.SPANX)
+ val indexSpanY: Int = cursor.getColumnIndexOrThrow(Favorites.SPANY)
+ val indexId: Int = cursor.getColumnIndexOrThrow(Favorites._ID)
+ val indexScreen: Int = cursor.getColumnIndexOrThrow(Favorites.SCREEN)
+ val indexTitle: Int = cursor.getColumnIndexOrThrow(Favorites.TITLE)
+ val indexAppWidgetId: Int = cursor.getColumnIndexOrThrow(Favorites.APPWIDGET_ID)
+ val indexWidgetProvider: Int = cursor.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER)
+ val indexIntent: Int = cursor.getColumnIndexOrThrow(Favorites.INTENT)
+ val indexItemType: Int = cursor.getColumnIndexOrThrow(Favorites.ITEM_TYPE)
+ val container: Int = cursor.getColumnIndexOrThrow(Favorites.CONTAINER)
+ while (cursor.moveToNext()) {
+ result.add(
+ WorkspaceItem(
+ x = cursor.getInt(indexCellX),
+ y = cursor.getInt(indexCellY),
+ spanX = cursor.getInt(indexSpanX),
+ spanY = cursor.getInt(indexSpanY),
+ id = cursor.getInt(indexId),
+ screenId = cursor.getInt(indexScreen),
+ title = cursor.getString(indexTitle),
+ appWidgetId = cursor.getInt(indexAppWidgetId),
+ appWidgetProvider = cursor.getString(indexWidgetProvider),
+ intent = cursor.getString(indexIntent),
+ type = cursor.getInt(indexItemType),
+ container = cursor.getInt(container)
+ )
+ )
+ }
+ return result
+}
+
+data class WorkspaceItem(
+ val x: Int,
+ val y: Int,
+ val spanX: Int,
+ val spanY: Int,
+ val id: Int,
+ val screenId: Int,
+ val title: String,
+ val appWidgetId: Int,
+ val appWidgetProvider: String,
+ val intent: String,
+ val type: Int,
+ val container: Int,
+)
diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
new file mode 100644
index 0000000..1002976
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 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.gridmigration
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.graphics.Point
+import android.os.Process
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.LauncherSettings.Favorites
+import com.android.launcher3.celllayout.testgenerator.ValidGridMigrationTestCaseGenerator
+import com.android.launcher3.celllayout.testgenerator.generateItemsForTest
+import com.android.launcher3.model.DatabaseHelper
+import com.android.launcher3.model.DeviceGridState
+import com.android.launcher3.model.GridSizeMigrationUtil
+import com.android.launcher3.pm.UserCache
+import com.android.launcher3.provider.LauncherDbUtils
+import com.android.launcher3.util.rule.TestStabilityRule
+import com.android.launcher3.util.rule.TestStabilityRule.Stability
+import java.util.Random
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ValidGridMigrationUnitTest {
+
+ companion object {
+ const val SEED = 1044542
+ const val REPEAT_AFTER = 10
+ const val TAG = "ValidGridMigrationUnitTest"
+ }
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ }
+
+ private fun validate(
+ srcItems: List<WorkspaceItem>,
+ dstItems: List<WorkspaceItem>,
+ destinationSize: Point
+ ) {
+ // This returns a map with the number of repeated elements
+ // ex { calculatorIcon : 6, weatherWidget : 2 }
+ val itemsToSet = { it: List<WorkspaceItem> ->
+ it.filter { it.container != Favorites.CONTAINER_HOTSEAT }
+ .groupingBy {
+ when (it.type) {
+ Favorites.ITEM_TYPE_FOLDER,
+ Favorites.ITEM_TYPE_APP_PAIR -> throw Exception("Not implemented")
+ Favorites.ITEM_TYPE_APPWIDGET -> it.appWidgetProvider
+ Favorites.ITEM_TYPE_APPLICATION -> it.intent
+ else -> it.title
+ }
+ }
+ .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"
+ }
+ assert(
+ (it.x + it.spanX in 0..destinationSize.x) &&
+ (it.y + it.spanY in 0..destinationSize.y)
+ ) {
+ "Item doesn't fit in the grid. Size = $destinationSize Item = $it"
+ }
+ }
+
+ assert(itemsToSet(srcItems) == itemsToSet(dstItems)) {
+ "The srcItems do not match the dstItems src = $srcItems dst = $dstItems"
+ }
+ }
+
+ private fun addItemsToDb(db: SQLiteDatabase, tableName: String, items: List<WorkspaceItem>) {
+ LauncherDbUtils.SQLiteTransaction(db).use { transaction ->
+ items.forEach { insertIntoDb(tableName, it, transaction.db) }
+ transaction.commit()
+ }
+ }
+
+ private fun migrate(
+ srcItems: List<WorkspaceItem>,
+ srcSize: Point,
+ targetSize: Point
+ ): List<WorkspaceItem> {
+ val userSerial = UserCache.INSTANCE[context].getSerialNumberForUser(Process.myUserHandle())
+ val dbHelper =
+ DatabaseHelper(
+ context,
+ null,
+ { 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)
+ 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
+ )
+ )
+ it.commit()
+ }
+ return readDb(dstTableName, dbHelper.readableDatabase)
+ }
+
+ @Test
+ fun runTestCase() {
+ val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
+ for (i in 0..50) {
+ val testCase = caseGenerator.generateTestCase()
+ 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)
+ }
+ }
+
+ // This test takes about 4 minutes, there is no need to run it in presubmit.
+ @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT)
+ @Test
+ fun runExtensiveTestCases() {
+ val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong()))
+ for (i in 0..1000) {
+ val testCase = caseGenerator.generateTestCase()
+ 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)
+ }
+ }
+}