diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java index e3c2d366c6..1dd7d4547c 100644 --- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java +++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java @@ -55,7 +55,7 @@ import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext; import com.android.launcher3.model.BaseLauncherBinder; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.Callbacks; -import com.android.launcher3.model.GridSizeMigrationUtil; +import com.android.launcher3.model.GridSizeMigrationDBController; import com.android.launcher3.model.LoaderTask; import com.android.launcher3.model.ModelDbController; import com.android.launcher3.provider.LauncherDbUtils; @@ -284,7 +284,7 @@ public class PreviewSurfaceRenderer { private void loadModelData() { final Context inflationContext = getPreviewContext(); final InvariantDeviceProfile idp = new InvariantDeviceProfile(inflationContext, mGridName); - if (GridSizeMigrationUtil.needsToMigrate(inflationContext, idp)) { + if (GridSizeMigrationDBController.needsToMigrate(inflationContext, idp)) { // Start the migration PreviewContext previewContext = new PreviewContext(inflationContext, idp); // Copy existing data to preview DB diff --git a/src/com/android/launcher3/model/DbEntry.java b/src/com/android/launcher3/model/DbEntry.java new file mode 100644 index 0000000000..c0c51da87b --- /dev/null +++ b/src/com/android/launcher3/model/DbEntry.java @@ -0,0 +1,155 @@ +/* + * 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; + +import android.content.ContentValues; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.util.ContentWriter; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class DbEntry extends ItemInfo implements Comparable { + + private static final String TAG = "DbEntry"; + + String mIntent; + String mProvider; + Map> 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) { + 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 instanceof DbEntry)) return false; + DbEntry entry = (DbEntry) o; + return Objects.equals(getEntryMigrationId(), entry.getEntryMigrationId()); + } + + @Override + public int hashCode() { + return Objects.hash(getEntryMigrationId()); + } + + /** + * Puts the updated DbEntry values into ContentValues which we then use to insert the + * entry to the DB. + */ + 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); + } + + @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. + */ + public String getEntryMigrationId() { + switch (itemType) { + case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: + case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: + return getFolderMigrationId(); + case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: + // 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 { + Intent i = Intent.parseUri(intentStr, 0); + return Objects.requireNonNull(i.getComponent()).toString(); + } catch (Exception e) { + return intentStr; + } + default: + return cleanIntentString(mIntent); + } + } + + /** + * This method should return an id that should be the same for two folders containing the + * same elements. + */ + @NonNull + private String getFolderMigrationId() { + return mFolderItems.keySet().stream() + .map(intentString -> mFolderItems.get(intentString).size() + + cleanIntentString(intentString)) + .sorted() + .collect(Collectors.joining(",")); + } + + /** + * This is needed because sourceBounds can change and make the id of two equal items + * different. + */ + @NonNull + private String cleanIntentString(@NonNull String intentStr) { + try { + Intent i = Intent.parseUri(intentStr, 0); + i.setSourceBounds(null); + return i.toURI(); + } catch (URISyntaxException e) { + Log.e(TAG, "Unable to parse Intent string", e); + return intentStr; + } + + } +} diff --git a/src/com/android/launcher3/model/GridSizeMigrationUtil.java b/src/com/android/launcher3/model/GridSizeMigrationDBController.java similarity index 83% rename from src/com/android/launcher3/model/GridSizeMigrationUtil.java rename to src/com/android/launcher3/model/GridSizeMigrationDBController.java index 4c017e9c0c..9531d5b14c 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationUtil.java +++ b/src/com/android/launcher3/model/GridSizeMigrationDBController.java @@ -28,7 +28,6 @@ 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.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; @@ -45,15 +44,12 @@ import com.android.launcher3.LauncherPrefs; import com.android.launcher3.LauncherSettings; import com.android.launcher3.Utilities; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.model.data.ItemInfo; 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; import com.android.launcher3.widget.WidgetManagerHelper; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -61,7 +57,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -69,12 +64,12 @@ import java.util.stream.Collectors; * This class takes care of shrinking the workspace (by maximum of one row and one column), as a * result of restoring from a larger device or device density change. */ -public class GridSizeMigrationUtil { +public class GridSizeMigrationDBController { - private static final String TAG = "GridSizeMigrationUtil"; + private static final String TAG = "GridSizeMigrationDBController"; private static final boolean DEBUG = true; - private GridSizeMigrationUtil() { + private GridSizeMigrationDBController() { // Util class should not be instantiated } @@ -85,7 +80,7 @@ public class GridSizeMigrationUtil { return needsToMigrate(new DeviceGridState(context), new DeviceGridState(idp)); } - private static boolean needsToMigrate( + static boolean needsToMigrate( DeviceGridState srcDeviceState, DeviceGridState destDeviceState) { boolean needsToMigrate = !destDeviceState.isCompatible(srcDeviceState); if (needsToMigrate) { @@ -95,6 +90,9 @@ public class GridSizeMigrationUtil { return needsToMigrate; } + /** + * @return all the workspace and hotseat entries in the db. + */ @VisibleForTesting public static List readAllEntries(SQLiteDatabase db, String tableName, Context context) { @@ -198,7 +196,7 @@ public class GridSizeMigrationUtil { Collectors.joining(",\n", "[", "]")) + "\n Removing Items:" + dstWorkspaceItems.stream().filter(entry -> - toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect( + toBeRemoved.contains(entry.id)).map(DbEntry::toString).collect( Collectors.joining(",\n", "[", "]")) + "\n Adding Workspace Items:" + workspaceToBeAdded.stream().map(DbEntry::toString).collect( @@ -291,7 +289,7 @@ public class GridSizeMigrationUtil { }); } - private static void insertEntryInDb(DatabaseHelper helper, DbEntry entry, + static void insertEntryInDb(DatabaseHelper helper, DbEntry entry, String srcTableName, String destTableName, List idsInUse) { int id = copyEntryAndUpdate(helper, entry, srcTableName, destTableName, idsInUse); if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER @@ -341,7 +339,7 @@ public class GridSizeMigrationUtil { return newId; } - private static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) { + static void removeEntryFromDb(SQLiteDatabase db, String tableName, IntArray entryIds) { db.delete(tableName, Utilities.createDbSelectionQuery(LauncherSettings.Favorites._ID, entryIds), null); } @@ -387,7 +385,7 @@ public class GridSizeMigrationUtil { private static boolean findPlacementForEntry(@NonNull final DbEntry entry, @NonNull final Point next, @NonNull final Point trg, @NonNull final GridOccupancy occupied, final int screenId) { - for (int y = next.y; y < trg.y; y++) { + for (int y = next.y; y < trg.y; y++) { for (int x = next.x; x < trg.x; x++) { boolean fits = occupied.isRegionVacant(x, y, entry.spanX, entry.spanY); boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX, @@ -413,7 +411,7 @@ public class GridSizeMigrationUtil { private static void solveHotseatPlacement( @NonNull final DatabaseHelper helper, final int hotseatSize, @NonNull final DbReader srcReader, @NonNull final DbReader destReader, - @NonNull final List placedHotseatItems, + @NonNull final List placedHotseatItems, @NonNull final List itemsToPlace, List idsInUse) { final boolean[] occupied = new boolean[hotseatSize]; @@ -436,15 +434,26 @@ public class GridSizeMigrationUtil { } } - @VisibleForTesting + static void copyCurrentGridToNewGrid( + @NonNull Context context, + @NonNull DeviceGridState destDeviceState, + @NonNull DatabaseHelper target, + @NonNull SQLiteDatabase source) { + // Only use this strategy when comparing the previous grid to the new grid and the + // columns are the same and the destination has more rows + copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context); + destDeviceState.writeToPrefs(context); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static class DbReader { - private final SQLiteDatabase mDb; - private final String mTableName; - private final Context mContext; - private int mLastScreenId = -1; + final SQLiteDatabase mDb; + final String mTableName; + final Context mContext; + int mLastScreenId = -1; - private final Map> mWorkspaceEntriesByScreenId = + final Map> mWorkspaceEntriesByScreenId = new ArrayMap<>(); public DbReader(SQLiteDatabase db, String tableName, Context context) { @@ -529,7 +538,7 @@ public class GridSizeMigrationUtil { LauncherSettings.Favorites.INTENT, // 7 LauncherSettings.Favorites.APPWIDGET_PROVIDER, // 8 LauncherSettings.Favorites.APPWIDGET_ID}, // 9 - LauncherSettings.Favorites.CONTAINER + " = " + LauncherSettings.Favorites.CONTAINER + " = " + LauncherSettings.Favorites.CONTAINER_DESKTOP); final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); @@ -648,118 +657,4 @@ public class GridSizeMigrationUtil { return mDb.query(mTableName, columns, where, null, null, null, null); } } - - public static class DbEntry extends ItemInfo implements Comparable { - - private String mIntent; - private String mProvider; - private Map> 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) { - 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(getEntryMigrationId(), entry.getEntryMigrationId()); - } - - @Override - public int hashCode() { - return Objects.hash(getEntryMigrationId()); - } - - 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); - } - - @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. - */ - public String getEntryMigrationId() { - switch (itemType) { - case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: - case LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR: - return getFolderMigrationId(); - case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: - // 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 { - Intent i = Intent.parseUri(intentStr, 0); - return Objects.requireNonNull(i.getComponent()).toString(); - } catch (Exception e) { - return intentStr; - } - default: - return cleanIntentString(mIntent); - } - } - - /** - * This method should return an id that should be the same for two folders containing the - * same elements. - */ - @NonNull - private String getFolderMigrationId() { - return mFolderItems.keySet().stream() - .map(intentString -> mFolderItems.get(intentString).size() - + cleanIntentString(intentString)) - .sorted() - .collect(Collectors.joining(",")); - } - - /** - * This is needed because sourceBounds can change and make the id of two equal items - * different. - */ - @NonNull - private String cleanIntentString(@NonNull String intentStr) { - try { - Intent i = Intent.parseUri(intentStr, 0); - i.setSourceBounds(null); - return i.toURI(); - } catch (URISyntaxException e) { - Log.e(TAG, "Unable to parse Intent string", e); - return intentStr; - } - - } - } } diff --git a/src/com/android/launcher3/model/GridSizeMigrationLogic.java b/src/com/android/launcher3/model/GridSizeMigrationLogic.java new file mode 100644 index 0000000000..12a14b223b --- /dev/null +++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.java @@ -0,0 +1,466 @@ +/* + * 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; + +import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle; +import static com.android.launcher3.LauncherPrefs.IS_FIRST_LOAD_AFTER_RESTORE; +import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; +import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE; +import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET; +import static com.android.launcher3.model.GridSizeMigrationDBController.copyCurrentGridToNewGrid; +import static com.android.launcher3.model.GridSizeMigrationDBController.insertEntryInDb; +import static com.android.launcher3.model.GridSizeMigrationDBController.needsToMigrate; +import static com.android.launcher3.model.GridSizeMigrationDBController.removeEntryFromDb; +import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN; +import static com.android.launcher3.provider.LauncherDbUtils.copyTable; +import static com.android.launcher3.provider.LauncherDbUtils.dropTable; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Point; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.launcher3.Flags; +import com.android.launcher3.LauncherPrefs; +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.provider.LauncherDbUtils; +import com.android.launcher3.util.CellAndSpan; +import com.android.launcher3.util.GridOccupancy; +import com.android.launcher3.util.IntArray; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class GridSizeMigrationLogic { + + private static final String TAG = "GridSizeMigrationLogic"; + private static final boolean DEBUG = true; + + /** + * Migrates the grid size from srcDeviceState to destDeviceState and make those changes + * in the target DB, using the source DB to determine what to add/remove/move/resize + * in the destination DB. + */ + public void migrateGrid( + @NonNull Context context, + @NonNull DeviceGridState srcDeviceState, + @NonNull DeviceGridState destDeviceState, + @NonNull DatabaseHelper target, + @NonNull SQLiteDatabase source) { + if (!needsToMigrate(srcDeviceState, destDeviceState)) { + return; + } + + boolean isFirstLoad = LauncherPrefs.get(context).get(IS_FIRST_LOAD_AFTER_RESTORE); + Log.d(TAG, "Begin grid migration. First load: " + isFirstLoad); + + // This is a special case where if the grid is the same amount of columns but a larger + // amount of rows we simply copy over the source grid to the destination grid, rather + // than undergoing the general grid migration. + if (shouldMigrateToStrictlyTallerGrid(isFirstLoad, srcDeviceState, destDeviceState)) { + copyCurrentGridToNewGrid(context, destDeviceState, target, source); + return; + } + copyTable(source, TABLE_NAME, target.getWritableDatabase(), TMP_TABLE, context); + + long migrationStartTime = System.currentTimeMillis(); + try (LauncherDbUtils.SQLiteTransaction t = + new LauncherDbUtils.SQLiteTransaction(target.getWritableDatabase())) { + GridSizeMigrationDBController.DbReader srcReader = new GridSizeMigrationDBController + .DbReader(t.getDb(), TMP_TABLE, context); + GridSizeMigrationDBController.DbReader destReader = + new GridSizeMigrationDBController.DbReader( + t.getDb(), TABLE_NAME, context); + + Point targetSize = new Point(destDeviceState.getColumns(), destDeviceState.getRows()); + // Migrate hotseat. + migrateHotseat(destDeviceState.getNumHotseat(), srcReader, destReader, target); + // Migrate workspace. + migrateWorkspace(srcReader, destReader, target, targetSize); + + dropTable(t.getDb(), TMP_TABLE); + t.commit(); + } catch (Exception e) { + Log.e(TAG, "Error during grid migration", e); + } finally { + Log.v(TAG, "Workspace migration completed in " + + (System.currentTimeMillis() - migrationStartTime)); + + // Save current configuration, so that the migration does not run again. + destDeviceState.writeToPrefs(context); + } + } + + private void migrateHotseat(int destHotseatSize, + GridSizeMigrationDBController.DbReader srcReader, + GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper) { + final List srcHotseatItems = + srcReader.loadHotseatEntries(); + final List dstHotseatItems = + destReader.loadHotseatEntries(); + + + final List hotseatToBeAdded = + getItemsToBeAdded(srcHotseatItems, dstHotseatItems); + final IntArray toBeRemoved = new IntArray(); + toBeRemoved.addAll(getItemsToBeRemoved(srcHotseatItems, dstHotseatItems)); + + if (DEBUG) { + Log.d(TAG, "Start hotseat migration:" + + "\n Removing Hotseat Items:" + + dstHotseatItems.stream().filter(entry -> toBeRemoved + .contains(entry.id)).map(DbEntry::toString) + .collect(Collectors.joining(",\n", "[", "]")) + + "\n Adding Hotseat Items:" + + hotseatToBeAdded.stream().map(DbEntry::toString) + .collect(Collectors.joining(",\n", "[", "]")) + ); + } + + // Removes the items that we need to remove from the destination DB. + if (!toBeRemoved.isEmpty()) { + removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved); + } + + placeHotseatItems( + hotseatToBeAdded, dstHotseatItems, destHotseatSize, helper, srcReader, destReader); + } + + private void placeHotseatItems(List hotseatToBeAdded, + List dstHotseatItems, int destHotseatSize, + DatabaseHelper helper, GridSizeMigrationDBController.DbReader srcReader, + GridSizeMigrationDBController.DbReader destReader) { + if (hotseatToBeAdded.isEmpty()) { + return; + } + + List idsInUse = dstHotseatItems.stream().map(entry -> entry.id).toList(); + + Collections.sort(hotseatToBeAdded); + + List placementSolutionHotseat = + solveHotseatPlacement(destHotseatSize, dstHotseatItems, hotseatToBeAdded); + for (DbEntry entryToPlace: placementSolutionHotseat) { + insertEntryInDb(helper, entryToPlace, srcReader.mTableName, destReader.mTableName, + idsInUse); + } + } + + private void migrateWorkspace(GridSizeMigrationDBController.DbReader srcReader, + GridSizeMigrationDBController.DbReader destReader, DatabaseHelper helper, + Point targetSize) { + + + final List srcWorkspaceItems = + srcReader.loadAllWorkspaceEntries(); + + final List dstWorkspaceItems = + destReader.loadAllWorkspaceEntries(); + + final IntArray toBeRemoved = new IntArray(); + + List workspaceToBeAdded = + getItemsToBeAdded(srcWorkspaceItems, dstWorkspaceItems); + toBeRemoved.addAll(getItemsToBeRemoved(srcWorkspaceItems, dstWorkspaceItems)); + + if (DEBUG) { + Log.d(TAG, "Start workspace 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 Workspace 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", "[", "]")) + ); + } + + // Removes the items that we need to remove from the destination DB. + if (!toBeRemoved.isEmpty()) { + removeEntryFromDb(destReader.mDb, destReader.mTableName, toBeRemoved); + } + + placeWorkspaceItems(workspaceToBeAdded, dstWorkspaceItems, targetSize.x, targetSize.y, + helper, srcReader, destReader); + } + + private void placeWorkspaceItems( + List workspaceToBeAdded, + List dstWorkspaceItems, + int trgX, int trgY, DatabaseHelper helper, + GridSizeMigrationDBController.DbReader srcReader, + GridSizeMigrationDBController.DbReader destReader) { + if (workspaceToBeAdded.isEmpty()) { + return; + } + + List idsInUse = dstWorkspaceItems.stream().map(entry -> entry.id).collect( + Collectors.toList()); + + Collections.sort(workspaceToBeAdded); + + + // First we create a collection of the screens + List screens = new ArrayList<>(); + for (int screenId = 0; screenId <= destReader.mLastScreenId; screenId++) { + screens.add(screenId); + } + + // Then we place the items on the screens + WorkspaceItemsToPlace itemsToPlace = + new WorkspaceItemsToPlace(workspaceToBeAdded); + for (int screenId : screens) { + if (DEBUG) { + Log.d(TAG, "Migrating " + screenId); + } + itemsToPlace = solveGridPlacement( + destReader.mContext, screenId, trgX, trgY, itemsToPlace.mRemainingItemsToPlace, + destReader.mWorkspaceEntriesByScreenId.get(screenId)); + placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse); + while (!itemsToPlace.mPlacementSolution.isEmpty()) { + insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0), + srcReader.mTableName, destReader.mTableName, idsInUse); + } + if (itemsToPlace.mRemainingItemsToPlace.isEmpty()) { + break; + } + } + + // 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 (!itemsToPlace.mRemainingItemsToPlace.isEmpty()) { + itemsToPlace = solveGridPlacement(destReader.mContext, screenId, + trgX, trgY, itemsToPlace.mRemainingItemsToPlace, + destReader.mWorkspaceEntriesByScreenId.get(screenId)); + placeItems(itemsToPlace, helper, srcReader, destReader, idsInUse); + screenId++; + } + } + + private void placeItems(WorkspaceItemsToPlace itemsToPlace, DatabaseHelper helper, + GridSizeMigrationDBController.DbReader srcReader, + GridSizeMigrationDBController.DbReader destReader, List idsInUse) { + while (!itemsToPlace.mPlacementSolution.isEmpty()) { + insertEntryInDb(helper, itemsToPlace.mPlacementSolution.remove(0), + srcReader.mTableName, destReader.mTableName, idsInUse); + } + } + + + /** + * Only migrate the grid in this manner if the target grid is taller and not wider. + */ + private boolean shouldMigrateToStrictlyTallerGrid(boolean isFirstLoad, + @NonNull DeviceGridState srcDeviceState, + @NonNull DeviceGridState destDeviceState) { + if (isFirstLoad + && Flags.enableGridMigrationFix() + && srcDeviceState.getColumns().equals(destDeviceState.getColumns()) + && srcDeviceState.getRows() < destDeviceState.getRows()) { + return true; + } + return false; + } + + /** + * Finds all the items that are in the old grid which aren't in the new grid, meaning they + * need to be added to the new grid. + * + * @return a list of DbEntry's which we need to add. + */ + private List getItemsToBeAdded( + @NonNull final List src, + @NonNull final List dest) { + Map entryCountDiff = + calcDiff(src, dest); + List toBeAdded = new ArrayList<>(); + src.forEach(entry -> { + if (entryCountDiff.get(entry) > 0) { + toBeAdded.add(entry); + entryCountDiff.put(entry, entryCountDiff.get(entry) - 1); + } + }); + return toBeAdded; + } + + /** + * Finds all the items that are in the new grid which aren't in the old grid, meaning they + * need to be removed from the new grid. + * + * @return an IntArray of item id's which we need to remove. + */ + private IntArray getItemsToBeRemoved( + @NonNull final List src, + @NonNull final List dest) { + Map entryCountDiff = + calcDiff(src, dest); + IntArray toBeRemoved = new IntArray(); + dest.forEach(entry -> { + if (entryCountDiff.get(entry) < 0) { + toBeRemoved.add(entry.id); + if (entry.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) { + entry.mFolderItems.values().forEach(ids -> ids.forEach(toBeRemoved::add)); + } + entryCountDiff.put(entry, entryCountDiff.get(entry) + 1); + } + }); + return toBeRemoved; + } + + /** + * Calculates the difference between the old and new grid items in terms of how many of each + * item there are. E.g. if the old grid had 2 Calculator icons but the new grid has 0, then the + * difference there would be 2. While if the old grid has 0 Calculator icons and the + * new grid has 1, then the difference would be -1. + * + * @return a Map with each DbEntry as a key and the count of said entry as the value. + */ + private Map calcDiff( + @NonNull final List src, + @NonNull final List dest) { + Map entryCountDiff = new HashMap<>(); + src.forEach(entry -> + entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) + 1)); + dest.forEach(entry -> + entryCountDiff.put(entry, entryCountDiff.getOrDefault(entry, 0) - 1)); + return entryCountDiff; + } + + private List solveHotseatPlacement(final int hotseatSize, + @NonNull final List placedHotseatItems, + @NonNull final List itemsToPlace) { + List placementSolution = new ArrayList<>(); + List remainingItemsToPlace = + new ArrayList<>(itemsToPlace); + final boolean[] occupied = new boolean[hotseatSize]; + for (DbEntry entry : placedHotseatItems) { + occupied[entry.screenId] = true; + } + + for (int i = 0; i < occupied.length; i++) { + if (!occupied[i] && !remainingItemsToPlace.isEmpty()) { + DbEntry entry = remainingItemsToPlace.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; + + placementSolution.add(entry); + occupied[entry.screenId] = true; + } + } + return placementSolution; + } + + private WorkspaceItemsToPlace solveGridPlacement( + Context context, + final int screenId, final int trgX, final int trgY, + @NonNull final List sortedItemsToPlace, + List existedEntries) { + WorkspaceItemsToPlace itemsToPlace = new WorkspaceItemsToPlace(sortedItemsToPlace); + final GridOccupancy occupied = new GridOccupancy(trgX, trgY); + final Point trg = new Point(trgX, trgY); + final Point next = new Point(0, screenId == 0 + && (FeatureFlags.QSB_ON_FIRST_SCREEN + && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs(context) + .getBoolean(SMARTSPACE_ON_HOME_SCREEN, true)) + && !SHOULD_SHOW_FIRST_PAGE_WIDGET) + ? 1 /* smartspace */ : 0); + if (existedEntries != null) { + for (DbEntry entry : existedEntries) { + occupied.markCells(entry, true); + } + } + Iterator iterator = + itemsToPlace.mRemainingItemsToPlace.iterator(); + while (iterator.hasNext()) { + final DbEntry entry = iterator.next(); + if (entry.minSpanX > trgX || entry.minSpanY > trgY) { + iterator.remove(); + continue; + } + CellAndSpan placement = findPlacementForEntry( + entry, next.x, next.y, trg, occupied); + if (placement != null) { + entry.screenId = screenId; + entry.cellX = placement.cellX; + entry.cellY = placement.cellY; + entry.spanX = placement.spanX; + entry.spanY = placement.spanY; + occupied.markCells(entry, true); + next.set(entry.cellX + entry.spanX, entry.cellY); + itemsToPlace.mPlacementSolution.add(entry); + iterator.remove(); + } + } + return itemsToPlace; + } + + /** + * Search for the next possible placement of an item. (mNextStartX, mNextStartY) serves as + * a memoization of last placement, we can start our search for next placement from there + * to speed up the search. + * + * @return NewEntryPlacement object if we found a valid placement, null if we didn't. + */ + private CellAndSpan findPlacementForEntry( + @NonNull final DbEntry entry, + int startPosX, int startPosY, @NonNull final Point trg, + @NonNull final GridOccupancy occupied) { + for (int y = startPosY; y < trg.y; y++) { + for (int x = startPosX; x < trg.x; x++) { + boolean minFits = occupied.isRegionVacant(x, y, entry.minSpanX, entry.minSpanY); + if (minFits) { + return (new CellAndSpan(x, y, entry.minSpanX, entry.minSpanY)); + } + } + startPosX = 0; + } + return null; + } + + private static class WorkspaceItemsToPlace { + List mRemainingItemsToPlace; + List mPlacementSolution; + + WorkspaceItemsToPlace(List sortedItemsToPlace) { + mRemainingItemsToPlace = new ArrayList<>(sortedItemsToPlace); + mPlacementSolution = new ArrayList<>(); + } + + } +} diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 09d1146932..b0108c274e 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -435,7 +435,15 @@ public class LoaderTask implements Runnable { final WidgetInflater widgetInflater = new WidgetInflater(context); ModelDbController dbController = mApp.getModel().getModelDbController(); - dbController.tryMigrateDB(restoreEventLogger); + if (Flags.gridMigrationRefactor()) { + try { + dbController.attemptMigrateDb(restoreEventLogger); + } catch (Exception e) { + FileLog.e(TAG, "Failed to migrate grid", e); + } + } else { + dbController.tryMigrateDB(restoreEventLogger); + } Log.d(TAG, "loadWorkspace: loading default favorites"); dbController.loadDefaultFavoritesIfNecessary(); diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java index 787aef4da5..4f0f1621f6 100644 --- a/src/com/android/launcher3/model/ModelDbController.java +++ b/src/com/android/launcher3/model/ModelDbController.java @@ -290,6 +290,100 @@ public class ModelDbController { } + /** + * Resets the launcher DB if we should reset it. + */ + public void resetLauncherDb(@Nullable LauncherRestoreEventLogger restoreEventLogger) { + if (restoreEventLogger != null) { + sendMetricsForFailedMigration(restoreEventLogger, getDb()); + } + FileLog.d(TAG, "Migration failed: resetting launcher database"); + createEmptyDB(); + LauncherPrefs.get(mContext).putSync( + getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true)); + + // Write the grid state to avoid another migration + new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext); + } + + /** + * Determines if we should reset the DB. + */ + private boolean shouldResetDb() { + if (isThereExistingDb()) { + return true; + } + if (!isGridMigrationNecessary()) { + return false; + } + if (isCurrentDbSameAsTarget()) { + return true; + } + return false; + } + + private boolean isThereExistingDb() { + if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) { + // If we already have a new DB, ignore migration + Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration"); + return true; + } + return false; + } + + private boolean isGridMigrationNecessary() { + InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); + if (GridSizeMigrationDBController.needsToMigrate(mContext, idp)) { + return true; + } + Log.d(TAG, "migrateGridIfNeeded: no grid migration needed"); + return false; + } + + private boolean isCurrentDbSameAsTarget() { + InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); + String targetDbName = new DeviceGridState(idp).getDbFile(); + if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) { + Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName); + return true; + } + return false; + } + + /** + * Migrates the DB. If the migration failed, it clears the DB. + */ + public void attemptMigrateDb(LauncherRestoreEventLogger restoreEventLogger) throws Exception { + createDbIfNotExists(); + + if (shouldResetDb()) { + resetLauncherDb(restoreEventLogger); + return; + } + + InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); + DatabaseHelper oldHelper = mOpenHelper; + mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper + : createDatabaseHelper(true /* forMigration */); + try { + // This is the current grid we have, given by the mContext + DeviceGridState srcDeviceState = new DeviceGridState(mContext); + // This is the state we want to migrate to that is given by the idp + DeviceGridState destDeviceState = new DeviceGridState(idp); + + GridSizeMigrationLogic gridSizeMigrationLogic = new GridSizeMigrationLogic(); + gridSizeMigrationLogic.migrateGrid(mContext, srcDeviceState, destDeviceState, + mOpenHelper, oldHelper.getWritableDatabase()); + } catch (Exception e) { + resetLauncherDb(restoreEventLogger); + throw new Exception("Failed to migrate grid", e); + } finally { + if (mOpenHelper != oldHelper) { + oldHelper.close(); + } + } + } + /** * Migrates the DB if needed. If the migration failed, it clears the DB. */ @@ -333,8 +427,8 @@ public class ModelDbController { return false; } InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext); - if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) { - FileLog.d(TAG, "migrateGridIfNeeded: no grid migration needed"); + if (!GridSizeMigrationDBController.needsToMigrate(mContext, idp)) { + Log.d(TAG, "migrateGridIfNeeded: no grid migration needed"); return true; } String targetDbName = new DeviceGridState(idp).getDbFile(); @@ -350,7 +444,7 @@ public class ModelDbController { DeviceGridState srcDeviceState = new DeviceGridState(mContext); // This is the state we want to migrate to that is given by the idp DeviceGridState destDeviceState = new DeviceGridState(idp); - return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, srcDeviceState, + return GridSizeMigrationDBController.migrateGridIfNeeded(mContext, srcDeviceState, destDeviceState, mOpenHelper, oldHelper.getWritableDatabase()); } catch (Exception e) { FileLog.e(TAG, "Failed to migrate grid", e); diff --git a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java index 0c3081fa83..a9082e2a7a 100644 --- a/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java +++ b/tests/multivalentTests/src/com/android/launcher3/celllayout/FavoriteItemsTransaction.java @@ -22,6 +22,7 @@ import static com.android.launcher3.util.TestUtil.runOnExecutorSync; import android.content.Context; +import com.android.launcher3.Flags; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.LauncherSettings; @@ -59,7 +60,11 @@ public class FavoriteItemsTransaction { runOnExecutorSync(MODEL_EXECUTOR, () -> { ModelDbController controller = model.getModelDbController(); // Migrate any previous data so that the DB state is correct - controller.tryMigrateDB(null /* restoreEventLogger */); + if (Flags.gridMigrationRefactor()) { + controller.attemptMigrateDb(null /* restoreEventLogger */); + } else { + controller.tryMigrateDB(null /* restoreEventLogger */); + } // Create DB again to load fresh data controller.createEmptyDB(); diff --git a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt similarity index 83% rename from tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt rename to tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt index f57e8a12de..c6f291d09b 100644 --- a/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationUtilTest.kt +++ b/tests/multivalentTests/src/com/android/launcher3/model/GridSizeMigrationDBControllerTest.kt @@ -22,13 +22,16 @@ import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.graphics.Point import android.os.Process +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.launcher3.Flags import com.android.launcher3.InvariantDeviceProfile import com.android.launcher3.LauncherPrefs import com.android.launcher3.LauncherPrefs.Companion.WORKSPACE_SIZE import com.android.launcher3.LauncherSettings.Favorites.* -import com.android.launcher3.model.GridSizeMigrationUtil.DbReader +import com.android.launcher3.model.GridSizeMigrationDBController.DbReader import com.android.launcher3.pm.UserCache import com.android.launcher3.provider.LauncherDbUtils import com.android.launcher3.util.LauncherModelHelper @@ -82,9 +85,22 @@ class GridSizeMigrationUtilTest { modelHelper.destroy() } - /** Old migration logic, should be modified once is not needed anymore */ @Test @Throws(Exception::class) + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun testMigrationRefactorFlagOn() { + testMigration() + } + + @Test + @Throws(Exception::class) + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun testMigrationRefactorFlagOff() { + testMigration() + } + + /** Old migration logic, should be modified once is not needed anymore */ + @Throws(Exception::class) fun testMigration() { // Src Hotseat icons addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE) @@ -113,15 +129,26 @@ class GridSizeMigrationUtilTest { idp.numRows = 4 val srcReader = DbReader(db, TMP_TABLE, context) val destReader = DbReader(db, TABLE_NAME, context) - GridSizeMigrationUtil.migrate( - dbHelper, - srcReader, - destReader, - idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), - ) + if (Flags.gridMigrationRefactor()) { + val gridSizeMigrationLogic = GridSizeMigrationLogic() + gridSizeMigrationLogic.migrateGrid( + context, + DeviceGridState(context), + DeviceGridState(idp), + dbHelper, + db, + ) + } else { + GridSizeMigrationDBController.migrate( + dbHelper, + srcReader, + destReader, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows), + DeviceGridState(context), + DeviceGridState(idp), + ) + } // Check hotseat items var c = @@ -187,9 +214,22 @@ class GridSizeMigrationUtilTest { assertThat(locMap[testPackage9]).isEqualTo(Point(0, 2)) } - /** Old migration logic, should be modified once is not needed anymore */ @Test @Throws(Exception::class) + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun testMigrationBackAndForthRefactorFlagOn() { + testMigrationBackAndForth() + } + + @Test + @Throws(Exception::class) + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun testMigrationBackAndForthRefactorFlagOff() { + testMigrationBackAndForth() + } + + /** Old migration logic, should be modified once is not needed anymore */ + @Throws(Exception::class) fun testMigrationBackAndForth() { // Hotseat items in grid A // 1 2 _ 3 4 @@ -224,15 +264,26 @@ class GridSizeMigrationUtilTest { val readerGridA = DbReader(db, TMP_TABLE, context) val readerGridB = DbReader(db, TABLE_NAME, context) // migrate from A -> B - GridSizeMigrationUtil.migrate( - dbHelper, - readerGridA, - readerGridB, - idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), - ) + if (Flags.gridMigrationRefactor()) { + var gridSizeMigrationLogic = GridSizeMigrationLogic() + gridSizeMigrationLogic.migrateGrid( + context, + DeviceGridState(context), + DeviceGridState(idp), + dbHelper, + db, + ) + } else { + GridSizeMigrationDBController.migrate( + dbHelper, + readerGridA, + readerGridB, + idp.numDatabaseHotseatIcons, + Point(idp.numColumns, idp.numRows), + DeviceGridState(context), + DeviceGridState(idp), + ) + } // Check hotseat items in grid B var c = @@ -280,15 +331,8 @@ class GridSizeMigrationUtilTest { addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_DESKTOP, 0, 2, testPackage9) // migrate from B -> A - GridSizeMigrationUtil.migrate( - dbHelper, - readerGridB, - readerGridA, - 5, - Point(5, 5), - DeviceGridState(idp), - DeviceGridState(context), - ) + migrateGrid(dbHelper, readerGridB, readerGridA, 5, 5, 5) + // Check hotseat items in grid A c = db.query( @@ -339,14 +383,13 @@ class GridSizeMigrationUtilTest { db.delete(TMP_TABLE, "$_ID=7", null) // migrate from A -> B - GridSizeMigrationUtil.migrate( + migrateGrid( dbHelper, readerGridA, readerGridB, idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), + idp.numColumns, + idp.numRows, ) // Check hotseat items in grid B @@ -392,6 +435,36 @@ class GridSizeMigrationUtilTest { assertThat(locMap[testPackage9]).isEqualTo(Triple(0, 0, 2)) } + private fun migrateGrid( + dbHelper: DatabaseHelper, + srcReader: DbReader, + destReader: DbReader, + destHotseatSize: Int, + pointX: Int, + pointY: Int, + ) { + if (Flags.gridMigrationRefactor()) { + var gridSizeMigrationLogic = GridSizeMigrationLogic() + gridSizeMigrationLogic.migrateGrid( + context, + DeviceGridState(context), + DeviceGridState(idp), + dbHelper, + db, + ) + } else { + GridSizeMigrationDBController.migrate( + dbHelper, + srcReader, + destReader, + destHotseatSize, + Point(pointX, pointY), + DeviceGridState(idp), + DeviceGridState(context), + ) + } + } + private fun verifyHotseat(c: Cursor, idp: InvariantDeviceProfile, expected: List) { assertThat(c.count).isEqualTo(idp.numDatabaseHotseatIcons) val screenIndex = c.getColumnIndex(SCREEN) @@ -421,6 +494,17 @@ class GridSizeMigrationUtilTest { } @Test + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateToLargerHotseatRefactorFlagOn() { + migrateToLargerHotseat() + } + + @Test + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateToLargerHotseatRefactorFlagOff() { + migrateToLargerHotseat() + } + fun migrateToLargerHotseat() { val srcHotseatItems = intArrayOf( @@ -471,14 +555,13 @@ class GridSizeMigrationUtilTest { idp.numRows = 4 val srcReader = DbReader(db, TMP_TABLE, context) val destReader = DbReader(db, TABLE_NAME, context) - GridSizeMigrationUtil.migrate( + migrateGrid( dbHelper, srcReader, destReader, idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), + idp.numColumns, + idp.numRows, ) // Check hotseat items @@ -516,6 +599,17 @@ class GridSizeMigrationUtilTest { } @Test + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromLargerHotseatRefactorFlagOn() { + migrateFromLargerHotseat() + } + + @Test + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromLargerHotseatRefactorFlagOff() { + migrateFromLargerHotseat() + } + fun migrateFromLargerHotseat() { addItem(ITEM_TYPE_APPLICATION, 0, CONTAINER_HOTSEAT, 0, 0, testPackage1, 1, TMP_TABLE) addItem(ITEM_TYPE_DEEP_SHORTCUT, 2, CONTAINER_HOTSEAT, 0, 0, testPackage2, 2, TMP_TABLE) @@ -528,14 +622,13 @@ class GridSizeMigrationUtilTest { idp.numRows = 4 val srcReader = DbReader(db, TMP_TABLE, context) val destReader = DbReader(db, TABLE_NAME, context) - GridSizeMigrationUtil.migrate( + migrateGrid( dbHelper, srcReader, destReader, idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), + idp.numColumns, + idp.numRows, ) // Check hotseat items @@ -573,11 +666,24 @@ class GridSizeMigrationUtilTest { c.close() } + @Test + @Throws(Exception::class) + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromSmallerGridBigDifferenceRefactorFlagOn() { + migrateFromSmallerGridBigDifference() + } + + @Test + @Throws(Exception::class) + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromSmallerGridBigDifferenceRefactorFlagOff() { + migrateFromSmallerGridBigDifference() + } + /** * Migrating from a smaller grid to a large one should reflow the pages if the column difference * is more than 2 */ - @Test @Throws(Exception::class) fun migrateFromSmallerGridBigDifference() { enableNewMigrationLogic("2,2") @@ -594,14 +700,13 @@ class GridSizeMigrationUtilTest { idp.numRows = 5 val srcReader = DbReader(db, TMP_TABLE, context) val destReader = DbReader(db, TABLE_NAME, context) - GridSizeMigrationUtil.migrate( + migrateGrid( dbHelper, srcReader, destReader, idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), + idp.numColumns, + idp.numRows, ) // Get workspace items @@ -636,9 +741,22 @@ class GridSizeMigrationUtilTest { assertThat(locMap[testPackage5]).isEqualTo(0) } - /** Migrating from a larger grid to a smaller, we reflow from page 0 */ @Test @Throws(Exception::class) + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromLargerGridRefactorFlagOn() { + migrateFromLargerGrid() + } + + @Test + @Throws(Exception::class) + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun migrateFromLargerGridRefactorFlagOff() { + migrateFromLargerGrid() + } + + /** Migrating from a larger grid to a smaller, we reflow from page 0 */ + @Throws(Exception::class) fun migrateFromLargerGrid() { enableNewMigrationLogic("5,5") @@ -654,14 +772,13 @@ class GridSizeMigrationUtilTest { idp.numRows = 4 val srcReader = DbReader(db, TMP_TABLE, context) val destReader = DbReader(db, TABLE_NAME, context) - GridSizeMigrationUtil.migrate( + migrateGrid( dbHelper, srcReader, destReader, idp.numDatabaseHotseatIcons, - Point(idp.numColumns, idp.numRows), - DeviceGridState(context), - DeviceGridState(idp), + idp.numColumns, + idp.numRows, ) // Get workspace items diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt index 6bd182b1fe..8d072d8014 100644 --- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt +++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt @@ -1,6 +1,7 @@ package com.android.launcher3.util import android.content.ContentValues +import com.android.launcher3.Flags import com.android.launcher3.LauncherModel import com.android.launcher3.LauncherSettings.Favorites import com.android.launcher3.LauncherSettings.Favorites.APPWIDGET_ID @@ -30,7 +31,8 @@ object ModelTestExtensions { loadModelSync() TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { modelDbController.run { - tryMigrateDB(null /* restoreEventLogger */) + if (Flags.gridMigrationRefactor()) attemptMigrateDb(null /* restoreEventLogger */) + else tryMigrateDB(null /* restoreEventLogger */) createEmptyDB() clearEmptyDbFlag() } @@ -67,12 +69,12 @@ object ModelTestExtensions { tableName: String = Favorites.TABLE_NAME, appWidgetId: Int = -1, appWidgetSource: Int = -1, - appWidgetProvider: String? = null + appWidgetProvider: String? = null, ) { loadModelSync() TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { val controller: ModelDbController = modelDbController - controller.tryMigrateDB(null /* restoreEventLogger */) + controller.attemptMigrateDb(null /* restoreEventLogger */) modelDbController.newTransaction().use { transaction -> val values = ContentValues().apply { diff --git a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt index 35ac0a1027..b4ee090d14 100644 --- a/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt +++ b/tests/src/com/android/launcher3/backuprestore/BackupAndRestoreDBSelectionTest.kt @@ -16,6 +16,8 @@ package com.android.launcher3.backuprestore +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest @@ -52,10 +54,24 @@ class BackupAndRestoreDBSelectionTest { setFlagsRule.setFlags(true, Flags.FLAG_ENABLE_NARROW_GRID_RESTORE) } + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun oldDatabasesNotPresentAfterRestoreRefactorFlagEnabled() { + oldDatabasesNotPresentAfterRestore() + } + + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun oldDatabasesNotPresentAfterRestoreRefactorFlagDisabled() { + oldDatabasesNotPresentAfterRestore() + } + @Test fun oldDatabasesNotPresentAfterRestore() { val dbController = ModelDbController(getInstrumentation().targetContext) - dbController.tryMigrateDB(null) + if (Flags.gridMigrationRefactor()) { + dbController.attemptMigrateDb(null) + } else { + dbController.tryMigrateDB(null) + } TestUtil.runOnExecutorSync(MODEL_EXECUTOR) { assert(backAndRestoreRule.getDatabaseFiles().size == 1) { "There should only be one database after restoring, the last one used. Actual databases ${backAndRestoreRule.getDatabaseFiles()}" diff --git a/tests/src/com/android/launcher3/model/GridMigrationTest.kt b/tests/src/com/android/launcher3/model/GridMigrationTest.kt index 15222a4fcf..666ec16eb2 100644 --- a/tests/src/com/android/launcher3/model/GridMigrationTest.kt +++ b/tests/src/com/android/launcher3/model/GridMigrationTest.kt @@ -52,11 +52,15 @@ class GridMigrationData(dbFileName: String?, val gridState: DeviceGridState) { phoneContext, dbFileName, { UserCache.INSTANCE.get(phoneContext).getSerialNumberForUser(it) }, - {} + {}, ) - fun readEntries(): List = - GridSizeMigrationUtil.readAllEntries(dbHelper.readableDatabase, TABLE_NAME, phoneContext) + fun readEntries(): List = + GridSizeMigrationDBController.readAllEntries( + dbHelper.readableDatabase, + TABLE_NAME, + phoneContext, + ) } /** @@ -80,7 +84,7 @@ class GridMigrationTest { TestToPhoneFileCopier( src = "databases/GridMigrationTest/$DB_FILE", dest = "databases/$DB_FILE", - removeOnFinish = true + removeOnFinish = true, ) @Before @@ -89,13 +93,24 @@ class GridMigrationTest { } private fun migrate(src: GridMigrationData, dst: GridMigrationData) { - GridSizeMigrationUtil.migrateGridIfNeeded( - phoneContext, - src.gridState, - dst.gridState, - dst.dbHelper, - src.dbHelper.readableDatabase - ) + if (Flags.gridMigrationRefactor()) { + val gridSizeMigrationLogic = GridSizeMigrationLogic() + gridSizeMigrationLogic.migrateGrid( + phoneContext, + src.gridState, + dst.gridState, + dst.dbHelper, + src.dbHelper.readableDatabase, + ) + } else { + GridSizeMigrationDBController.migrateGridIfNeeded( + phoneContext, + src.gridState, + dst.gridState, + dst.dbHelper, + src.dbHelper.readableDatabase, + ) + } } /** @@ -115,10 +130,8 @@ class GridMigrationTest { } private fun compare(dst: GridMigrationData, target: GridMigrationData) { - val sort = compareBy({ it.cellX }, { it.cellY }) - val mapF = { it: GridSizeMigrationUtil.DbEntry -> - EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank) - } + val sort = compareBy({ it.cellX }, { it.cellY }) + val mapF = { it: DbEntry -> EntryData(it.cellX, it.cellY, it.spanX, it.spanY, it.rank) } val entriesDst = dst.readEntries().sortedWith(sort).map(mapF) val entriesTarget = target.readEntries().sortedWith(sort).map(mapF) @@ -149,7 +162,7 @@ class GridMigrationTest { TestToPhoneFileCopier( src = "databases/GridMigrationTest/result5x5to3x3.db", dest = "databases/result5x5to3x3.db", - removeOnFinish = true + removeOnFinish = true, ) @Test @@ -160,10 +173,10 @@ class GridMigrationTest { GridMigrationData( null, // in memory db, to download a new db change null for the filename of the // db name to store it. Do not use existing names. - DeviceGridState(3, 3, 3, TYPE_PHONE, "") + DeviceGridState(3, 3, 3, TYPE_PHONE, ""), ), target = - GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, "")) + GridMigrationData("result5x5to3x3.db", DeviceGridState(3, 3, 3, TYPE_PHONE, "")), ) @JvmField @@ -172,7 +185,7 @@ class GridMigrationTest { TestToPhoneFileCopier( src = "databases/GridMigrationTest/result5x5to4x7.db", dest = "databases/result5x5to4x7.db", - removeOnFinish = true + removeOnFinish = true, ) @Test @@ -183,10 +196,10 @@ class GridMigrationTest { GridMigrationData( null, // in memory db, to download a new db change null for the filename of the // db name to store it. Do not use existing names. - DeviceGridState(4, 7, 4, TYPE_PHONE, "") + DeviceGridState(4, 7, 4, TYPE_PHONE, ""), ), target = - GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, "")) + GridMigrationData("result5x5to4x7.db", DeviceGridState(4, 7, 4, TYPE_PHONE, "")), ) @JvmField @@ -195,7 +208,7 @@ class GridMigrationTest { TestToPhoneFileCopier( src = "databases/GridMigrationTest/result5x5to5x8.db", dest = "databases/result5x5to5x8.db", - removeOnFinish = true + removeOnFinish = true, ) @Test @@ -206,10 +219,10 @@ class GridMigrationTest { GridMigrationData( null, // in memory db, to download a new db change null for the filename of the // db name to store it. Do not use existing names. - DeviceGridState(5, 8, 5, TYPE_PHONE, "") + DeviceGridState(5, 8, 5, TYPE_PHONE, ""), ), target = - GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, "")) + GridMigrationData("result5x5to5x8.db", DeviceGridState(5, 8, 5, TYPE_PHONE, "")), ) @JvmField @@ -218,7 +231,7 @@ class GridMigrationTest { TestToPhoneFileCopier( src = "databases/GridMigrationTest/flagged_result5x5to5x8.db", dest = "databases/flagged_result5x5to5x8.db", - removeOnFinish = true + removeOnFinish = true, ) @Test @@ -230,13 +243,13 @@ class GridMigrationTest { GridMigrationData( null, // in memory db, to download a new db change null for the filename of the // db name to store it. Do not use existing names. - DeviceGridState(5, 8, 5, TYPE_PHONE, "") + DeviceGridState(5, 8, 5, TYPE_PHONE, ""), ), target = GridMigrationData( "flagged_result5x5to5x8.db", - DeviceGridState(5, 8, 5, TYPE_PHONE, "") - ) + DeviceGridState(5, 8, 5, TYPE_PHONE, ""), + ), ) } } diff --git a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt index 03d01955ca..c08237ce4a 100644 --- a/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt +++ b/tests/src/com/android/launcher3/model/gridmigration/ValidGridMigrationUnitTest.kt @@ -20,17 +20,21 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import android.graphics.Point import android.os.Process +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags 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.Flags 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.model.GridSizeMigrationDBController +import com.android.launcher3.model.GridSizeMigrationLogic import com.android.launcher3.pm.UserCache import com.android.launcher3.provider.LauncherDbUtils import com.android.launcher3.util.rule.TestStabilityRule @@ -130,22 +134,44 @@ class ValidGridMigrationUnitTest { addItemsToDb(dbHelper.writableDatabase, dstGrid) LauncherDbUtils.SQLiteTransaction(dbHelper.writableDatabase).use { - GridSizeMigrationUtil.migrate( - dbHelper, - GridSizeMigrationUtil.DbReader(it.db, srcGrid.tableName, context), - GridSizeMigrationUtil.DbReader(it.db, dstGrid.tableName, context), - dstGrid.size.x, - dstGrid.size, - srcGrid.toGridState(), - dstGrid.toGridState(), - ) + if (Flags.gridMigrationRefactor()) { + val gridSizeMigrationLogic = GridSizeMigrationLogic() + gridSizeMigrationLogic.migrateGrid( + context, + srcGrid.toGridState(), + dstGrid.toGridState(), + dbHelper, + it.db, + ) + } else { + GridSizeMigrationDBController.migrate( + dbHelper, + GridSizeMigrationDBController.DbReader(it.db, srcGrid.tableName, context), + GridSizeMigrationDBController.DbReader(it.db, dstGrid.tableName, context), + dstGrid.size.x, + dstGrid.size, + srcGrid.toGridState(), + dstGrid.toGridState(), + ) + } it.commit() } return readDb(dstGrid.tableName, dbHelper.readableDatabase) } @Test - fun runTestCase() { + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun runTestCaseRefactorFlagEnabled() { + runTestCase() + } + + @Test + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun runTestCaseRefactorFlagDisabled() { + runTestCase() + } + + private fun runTestCase() { val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong())) for (i in 0..SMALL_TEST_SIZE) { val testCase = caseGenerator.generateTestCase(isDestEmpty = true) @@ -163,7 +189,18 @@ class ValidGridMigrationUnitTest { } @Test - fun mergeBoards() { + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun mergeBoardsRefactorFlagEnabled() { + mergeBoards() + } + + @Test + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun mergeBoardsRefactorFlagDisabled() { + mergeBoards() + } + + private fun mergeBoards() { val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong())) for (i in 0..SMALL_TEST_SIZE) { val testCase = caseGenerator.generateTestCase(isDestEmpty = false) @@ -187,7 +224,20 @@ class ValidGridMigrationUnitTest { // 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() { + @EnableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun runExtensiveTestCasesRefactorFlagEnabled() { + runExtensiveTestCases() + } + + // This test takes about 4 minutes, there is no need to run it in presubmit. + @Stability(flavors = TestStabilityRule.LOCAL or TestStabilityRule.PLATFORM_POSTSUBMIT) + @Test + @DisableFlags(Flags.FLAG_GRID_MIGRATION_REFACTOR) + fun runExtensiveTestCasesRefactorFlagDisabled() { + runExtensiveTestCases() + } + + private fun runExtensiveTestCases() { val caseGenerator = ValidGridMigrationTestCaseGenerator(Random(SEED.toLong())) for (i in 0..LARGE_TEST_SIZE) { val testCase = caseGenerator.generateTestCase(isDestEmpty = true)