Merge "Make grid size migration less confusing for users." into tm-qpr-dev

This commit is contained in:
TreeHugger Robot
2022-12-05 18:19:59 +00:00
committed by Android (Google) Code Review
3 changed files with 290 additions and 44 deletions

View File

@@ -66,7 +66,7 @@ import java.util.stream.Collectors;
public class GridSizeMigrationUtil {
private static final String TAG = "GridSizeMigrationUtil";
private static final boolean DEBUG = false;
private static final boolean DEBUG = true;
private GridSizeMigrationUtil() {
// Util class should not be instantiated
@@ -188,27 +188,54 @@ public class GridSizeMigrationUtil {
@NonNull final DeviceGridState srcDeviceState,
@NonNull final DeviceGridState destDeviceState) {
final List<DbEntry> hotseatItems = destReader.loadHotseatEntries();
final List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
final List<DbEntry> hotseatDiff =
calcDiff(srcReader.loadHotseatEntries(), hotseatItems);
final List<DbEntry> workspaceDiff =
calcDiff(srcReader.loadAllWorkspaceEntries(), workspaceItems);
final List<DbEntry> srcHotseatItems = srcReader.loadHotseatEntries();
final List<DbEntry> srcWorkspaceItems = srcReader.loadAllWorkspaceEntries();
final List<DbEntry> dstHotseatItems = destReader.loadHotseatEntries();
final List<DbEntry> dstWorkspaceItems = destReader.loadAllWorkspaceEntries();
final List<DbEntry> hotseatToBeAdded = new ArrayList<>(1);
final List<DbEntry> workspaceToBeAdded = new ArrayList<>(1);
final IntArray toBeRemoved = new IntArray();
calcDiff(srcHotseatItems, dstHotseatItems, hotseatToBeAdded, toBeRemoved);
calcDiff(srcWorkspaceItems, dstWorkspaceItems, workspaceToBeAdded, toBeRemoved);
final int trgX = targetSize.x;
final int trgY = targetSize.y;
if (hotseatDiff.isEmpty() && workspaceDiff.isEmpty()) {
if (DEBUG) {
Log.d(TAG, "Start migration:"
+ "\n Source Device:"
+ srcWorkspaceItems.stream().map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
+ "\n Target Device:"
+ dstWorkspaceItems.stream().map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
+ "\n Removing Items:"
+ dstWorkspaceItems.stream().filter(entry ->
toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
+ "\n Adding Workspace Items:"
+ workspaceToBeAdded.stream().map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
+ "\n Adding Hotseat Items:"
+ hotseatToBeAdded.stream().map(DbEntry::toString).collect(
Collectors.joining(",\n", "[", "]"))
);
}
if (!toBeRemoved.isEmpty()) {
removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved);
}
if (hotseatToBeAdded.isEmpty() && workspaceToBeAdded.isEmpty()) {
return false;
}
// Sort the items by the reading order.
Collections.sort(hotseatDiff);
Collections.sort(workspaceDiff);
Collections.sort(hotseatToBeAdded);
Collections.sort(workspaceToBeAdded);
// Migrate hotseat
solveHotseatPlacement(db, srcReader,
destReader, context, destHotseatSize, hotseatItems, hotseatDiff);
destReader, context, destHotseatSize, dstHotseatItems, hotseatToBeAdded);
// Migrate workspace.
// First we create a collection of the screens
@@ -229,8 +256,8 @@ public class GridSizeMigrationUtil {
Log.d(TAG, "Migrating " + screenId);
}
solveGridPlacement(db, srcReader,
destReader, context, screenId, trgX, trgY, workspaceDiff, false);
if (workspaceDiff.isEmpty()) {
destReader, context, screenId, trgX, trgY, workspaceToBeAdded, false);
if (workspaceToBeAdded.isEmpty()) {
break;
}
}
@@ -238,42 +265,37 @@ public class GridSizeMigrationUtil {
// In case the new grid is smaller, there might be some leftover items that don't fit on
// 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 (!workspaceDiff.isEmpty()) {
while (!workspaceToBeAdded.isEmpty()) {
solveGridPlacement(db, srcReader,
destReader, context, screenId, trgX, trgY, workspaceDiff, preservePages);
destReader, context, screenId, trgX, trgY, workspaceToBeAdded, preservePages);
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) {
Map<String, Integer> destIdSet = new HashMap<>();
for (DbEntry entry : dest) {
String entryID = entry.getEntryMigrationId();
if (destIdSet.containsKey(entryID)) {
destIdSet.put(entryID, destIdSet.get(entryID) + 1);
} else {
destIdSet.put(entryID, 1);
/**
* Calculate the differences between {@code src} (denoted by A) and {@code dest}
* (denoted by B).
* All DbEntry in A - B will be added to {@code toBeAdded}
* All DbEntry.id in B - A will be added to {@code toBeRemoved}
*/
private static void calcDiff(@NonNull final List<DbEntry> src,
@NonNull final List<DbEntry> dest, @NonNull final List<DbEntry> toBeAdded,
@NonNull final IntArray toBeRemoved) {
src.forEach(entry -> {
if (!dest.contains(entry)) {
toBeAdded.add(entry);
}
}
List<DbEntry> diff = new ArrayList<>();
for (DbEntry entry : src) {
String entryID = entry.getEntryMigrationId();
if (destIdSet.containsKey(entryID)) {
Integer count = destIdSet.get(entryID);
if (count <= 0) {
diff.add(entry);
destIdSet.remove(entryID);
} else {
destIdSet.put(entryID, count - 1);
});
dest.forEach(entry -> {
if (!src.contains(entry)) {
toBeRemoved.add(entry.id);
if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add));
}
} else {
diff.add(entry);
}
}
return diff;
});
}
private static void insertEntryInDb(SQLiteDatabase db, Context context, DbEntry entry,
@@ -682,12 +704,12 @@ public class GridSizeMigrationUtil {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DbEntry entry = (DbEntry) o;
return Objects.equals(mIntent, entry.mIntent);
return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId());
}
@Override
public int hashCode() {
return Objects.hash(mIntent);
return Objects.hash(getEntryMigrationId());
}
public void updateContentValues(ContentValues values) {

View File

@@ -17,6 +17,7 @@ package com.android.launcher3.model
import android.content.Context
import android.content.Intent
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase
import android.graphics.Point
import android.os.Process
@@ -183,15 +184,232 @@ class GridSizeMigrationUtilTest {
// Expected dest grid icons
// _ _ _ _
// 5 6 7 8
// 9 _ 10_
// 9 _ _ _
// _ _ _ _
assertThat(locMap.size.toLong()).isEqualTo(6)
assertThat(locMap.size.toLong()).isEqualTo(5)
assertThat(locMap[testPackage5]).isEqualTo(Point(0, 1))
assertThat(locMap[testPackage6]).isEqualTo(Point(1, 1))
assertThat(locMap[testPackage7]).isEqualTo(Point(2, 1))
assertThat(locMap[testPackage8]).isEqualTo(Point(3, 1))
assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2))
assertThat(locMap[testPackage10]).isEqualTo(Point(2, 2))
}
/**
* Old migration logic, should be modified once [FeatureFlags.ENABLE_NEW_MIGRATION_LOGIC] is
* not needed anymore
*/
@Test
@Throws(Exception::class)
fun testMigrationBackAndForth() {
// Hotseat items in grid A
// 1 2 _ 3 4
modelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI)
modelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI)
modelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI)
modelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI)
// Workspace items in grid A
// _ _ _ _ _
// _ _ _ _ 5
// _ _ 6 _ 7
// _ _ 8 _ _
// _ _ _ _ _
modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 1, testPackage5, 5, TMP_CONTENT_URI)
modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage6, 6, TMP_CONTENT_URI)
modelHelper.addItem(APP_ICON, 0, DESKTOP, 4, 2, testPackage7, 7, TMP_CONTENT_URI)
modelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 3, testPackage8, 8, TMP_CONTENT_URI)
// Hotseat items in grid B
// 2 _ _ _
modelHelper.addItem(SHORTCUT, 0, HOTSEAT, 0, 0, testPackage2)
// Workspace items in grid B
// _ _ _ _
// _ _ _ 10
// _ _ _ _
// _ _ _ _
modelHelper.addItem(APP_ICON, 0, DESKTOP, 1, 3, testPackage10)
idp.numDatabaseHotseatIcons = 4
idp.numColumns = 4
idp.numRows = 4
val readerGridA = DbReader(db, TMP_TABLE, context, validPackages)
val readerGridB = DbReader(db, TABLE_NAME, context, validPackages)
// migrate from A -> B
GridSizeMigrationUtil.migrate(
context,
db,
readerGridA,
readerGridB,
idp.numDatabaseHotseatIcons,
Point(idp.numColumns, idp.numRows),
DeviceGridState(context),
DeviceGridState(idp)
)
// Check hotseat items in grid B
var c = context.contentResolver.query(
CONTENT_URI,
arrayOf(SCREEN, INTENT),
"container=$CONTAINER_HOTSEAT",
null,
SCREEN,
null
) ?: throw IllegalStateException()
// Expected hotseat items in grid B
// 2 1 3 4
verifyHotseat(c, idp,
mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList())
// Check workspace items in grid B
c = context.contentResolver.query(
CONTENT_URI,
arrayOf(SCREEN, CELLX, CELLY, INTENT),
"container=$CONTAINER_DESKTOP",
null,
null,
null
) ?: throw IllegalStateException()
var locMap = parseLocMap(context, c)
// Expected items in grid B
// _ _ _ _
// 5 6 7 8
// _ _ _ _
// _ _ _ _
assertThat(locMap.size.toLong()).isEqualTo(4)
assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 0, 1))
assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 1, 1))
assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 2, 1))
assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))
// add item in B
modelHelper.addItem(APP_ICON, 0, DESKTOP, 0, 2, testPackage9)
// migrate from B -> A
GridSizeMigrationUtil.migrate(
context,
db,
readerGridB,
readerGridA,
5,
Point(5, 5),
DeviceGridState(idp),
DeviceGridState(context)
)
// Check hotseat items in grid A
c = context.contentResolver.query(
TMP_CONTENT_URI,
arrayOf(SCREEN, INTENT),
"container=$CONTAINER_HOTSEAT",
null,
SCREEN,
null
) ?: throw IllegalStateException()
// Expected hotseat items in grid A
// 1 2 _ 3 4
verifyHotseat(c, idp, mutableListOf(
testPackage1, testPackage2, null, testPackage3, testPackage4).toList())
// Check workspace items in grid A
c = context.contentResolver.query(
TMP_CONTENT_URI,
arrayOf(SCREEN, CELLX, CELLY, INTENT),
"container=$CONTAINER_DESKTOP",
null,
null,
null
) ?: throw IllegalStateException()
locMap = parseLocMap(context, c)
// Expected workspace items in grid A
// _ _ _ _ _
// _ _ _ _ 5
// 9 _ 6 _ 7
// _ _ 8 _ _
// _ _ _ _ _
assertThat(locMap.size.toLong()).isEqualTo(5)
// Verify items that existed in grid A remains in same position
assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 4, 1))
assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 2, 2))
assertThat(locMap[testPackage7]).isEqualTo(Triple(0, 4, 2))
assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 2, 3))
// Verify items that didn't exist in grid A are added in new screen
assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
// remove item from B
modelHelper.deleteItem(7, TMP_TABLE)
// migrate from A -> B
GridSizeMigrationUtil.migrate(
context,
db,
readerGridA,
readerGridB,
idp.numDatabaseHotseatIcons,
Point(idp.numColumns, idp.numRows),
DeviceGridState(context),
DeviceGridState(idp)
)
// Check hotseat items in grid B
c = context.contentResolver.query(
CONTENT_URI,
arrayOf(SCREEN, INTENT),
"container=$CONTAINER_HOTSEAT",
null,
SCREEN,
null
) ?: throw IllegalStateException()
// Expected hotseat items in grid B
// 2 1 3 4
verifyHotseat(c, idp,
mutableListOf(testPackage2, testPackage1, testPackage3, testPackage4).toList())
// Check workspace items in grid B
c = context.contentResolver.query(
CONTENT_URI,
arrayOf(SCREEN, CELLX, CELLY, INTENT),
"container=$CONTAINER_DESKTOP",
null,
null,
null
) ?: throw IllegalStateException()
locMap = parseLocMap(context, c)
// Expected workspace items in grid B
// _ _ _ _
// 5 6 _ 8
// 9 _ _ _
// _ _ _ _
assertThat(locMap.size.toLong()).isEqualTo(4)
assertThat(locMap[testPackage5]).isEqualTo(Triple(0, 0, 1))
assertThat(locMap[testPackage6]).isEqualTo(Triple(0, 1, 1))
assertThat(locMap[testPackage8]).isEqualTo(Triple(0, 3, 1))
assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2))
}
private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List<String?>) {
assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons)
val screenIndex = c.getColumnIndex(SCREEN)
val intentIndex = c.getColumnIndex(INTENT)
expected.forEachIndexed { idx, pkg ->
if (pkg == null) return@forEachIndexed
c.moveToNext()
assertThat(c.getInt(screenIndex).toLong()).isEqualTo(idx)
assertThat(c.getString(intentIndex)).contains(pkg)
}
c.close()
}
private fun parseLocMap(context: Context, c: Cursor): Map<String, Triple<Int, Int, Int>> {
// Check workspace items
val intentIndex = c.getColumnIndex(INTENT)
val screenIndex = c.getColumnIndex(SCREEN)
val cellXIndex = c.getColumnIndex(CELLX)
val cellYIndex = c.getColumnIndex(CELLY)
val locMap = mutableMapOf<String, Triple<Int, Int, Int>>()
while (c.moveToNext()) {
locMap[Intent.parseUri(c.getString(intentIndex), 0).getPackage()] =
Triple(c.getInt(screenIndex), c.getInt(cellXIndex), c.getInt(cellYIndex))
}
c.close()
return locMap.toMap()
}
@Test

View File

@@ -362,6 +362,12 @@ public class LauncherModelHelper {
sandboxContext.getContentResolver().insert(contentUri, values);
}
public void deleteItem(int itemId, @NonNull final String tableName) {
final Uri uri = Uri.parse("content://"
+ LauncherProvider.AUTHORITY + "/" + tableName + "/" + itemId);
sandboxContext.getContentResolver().delete(uri, null, null);
}
public int[][][] createGrid(int[][][] typeArray) {
return createGrid(typeArray, 1);
}