The new grid migration algorithm
go/grid-migration-v2
When changing grid from option 1 to option 2, we calculate the diff and add the icons that are in option 1 but not option 2, to option 2's workspace, according to the reading order.
Test: manual and unit tests
Fixes: 144052802
Change-Id: Id01f69e90ce656a9b7c9051fed499807ee9ac0f7
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
new file mode 100644
index 0000000..8f58d8b
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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.LauncherSettings.Favorites.CONTAINER_DESKTOP;
+import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
+import static com.android.launcher3.LauncherSettings.Favorites.TMP_CONTENT_URI;
+import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
+import static com.android.launcher3.util.LauncherModelHelper.APP_ICON;
+import static com.android.launcher3.util.LauncherModelHelper.DESKTOP;
+import static com.android.launcher3.util.LauncherModelHelper.HOTSEAT;
+import static com.android.launcher3.util.LauncherModelHelper.SHORTCUT;
+import static com.android.launcher3.util.LauncherModelHelper.TEST_PACKAGE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.os.Process;
+
+import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.pm.UserCache;
+import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.LauncherRoboTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.HashSet;
+
+/** Unit tests for {@link GridSizeMigrationTaskV2} */
+@RunWith(LauncherRoboTestRunner.class)
+public class GridSizeMigrationTaskV2Test {
+
+ private LauncherModelHelper mModelHelper;
+ private Context mContext;
+ private SQLiteDatabase mDb;
+
+ private HashSet<String> mValidPackages;
+ private InvariantDeviceProfile mIdp;
+
+ @Before
+ public void setUp() {
+ mModelHelper = new LauncherModelHelper();
+ mContext = RuntimeEnvironment.application;
+ mDb = mModelHelper.provider.getDb();
+
+ mValidPackages = new HashSet<>();
+ mValidPackages.add(TEST_PACKAGE);
+ mIdp = InvariantDeviceProfile.INSTANCE.get(mContext);
+
+ long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser(
+ Process.myUserHandle());
+ dropTable(mDb, LauncherSettings.Favorites.TMP_TABLE);
+ LauncherSettings.Favorites.addTableToDb(mDb, userSerial, false,
+ LauncherSettings.Favorites.TMP_TABLE);
+ }
+
+ @Test
+ public void testMigration() {
+ final String testPackage1 = "com.android.launcher3.validpackage1";
+ final String testPackage2 = "com.android.launcher3.validpackage2";
+ final String testPackage3 = "com.android.launcher3.validpackage3";
+ final String testPackage4 = "com.android.launcher3.validpackage4";
+ final String testPackage5 = "com.android.launcher3.validpackage5";
+ final String testPackage7 = "com.android.launcher3.validpackage7";
+
+ mValidPackages.add(testPackage1);
+ mValidPackages.add(testPackage2);
+ mValidPackages.add(testPackage3);
+ mValidPackages.add(testPackage4);
+ mValidPackages.add(testPackage5);
+ mValidPackages.add(testPackage7);
+
+ int[] srcHotseatItems = {
+ mModelHelper.addItem(APP_ICON, 0, HOTSEAT, 0, 0, testPackage1, 1, TMP_CONTENT_URI),
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2, 2, TMP_CONTENT_URI),
+ -1,
+ mModelHelper.addItem(SHORTCUT, 3, HOTSEAT, 0, 0, testPackage3, 3, TMP_CONTENT_URI),
+ mModelHelper.addItem(APP_ICON, 4, HOTSEAT, 0, 0, testPackage4, 4, TMP_CONTENT_URI),
+ };
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage5, 5, TMP_CONTENT_URI);
+
+ int[] destHotseatItems = {
+ -1,
+ mModelHelper.addItem(SHORTCUT, 1, HOTSEAT, 0, 0, testPackage2),
+ -1,
+ };
+ mModelHelper.addItem(APP_ICON, 0, DESKTOP, 2, 2, testPackage7);
+
+ mIdp.numHotseatIcons = 3;
+ mIdp.numColumns = 3;
+ mIdp.numRows = 3;
+ GridSizeMigrationTaskV2.DbReader srcReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TMP_TABLE, mContext, mValidPackages, 5);
+ GridSizeMigrationTaskV2.DbReader destReader = new GridSizeMigrationTaskV2.DbReader(mDb,
+ LauncherSettings.Favorites.TABLE_NAME, mContext, mValidPackages, 3);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
+ destReader, 3, new Point(mIdp.numColumns, mIdp.numRows));
+ task.migrate();
+
+ Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_HOTSEAT, null, null, null);
+ assertEquals(c.getCount(), 3);
+ int screenIndex = c.getColumnIndex(LauncherSettings.Favorites.SCREEN);
+ int intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 1);
+ assertTrue(c.getString(intentIndex).contains(testPackage2));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 0);
+ assertTrue(c.getString(intentIndex).contains(testPackage1));
+ c.moveToNext();
+ assertEquals(c.getInt(screenIndex), 2);
+ assertTrue(c.getString(intentIndex).contains(testPackage3));
+ c.close();
+
+ c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
+ new String[]{LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY,
+ LauncherSettings.Favorites.INTENT},
+ "container=" + CONTAINER_DESKTOP, null, null, null);
+ assertEquals(c.getCount(), 2);
+ intentIndex = c.getColumnIndex(LauncherSettings.Favorites.INTENT);
+ int cellXIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLX);
+ int cellYIndex = c.getColumnIndex(LauncherSettings.Favorites.CELLY);
+
+ c.moveToNext();
+ assertTrue(c.getString(intentIndex).contains(testPackage7));
+ c.moveToNext();
+ assertTrue(c.getString(intentIndex).contains(testPackage5));
+ assertEquals(c.getInt(cellXIndex), 0);
+ assertEquals(c.getInt(cellYIndex), 2);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
index e133cf2..20b1453 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -230,7 +230,21 @@
}
public int addItem(int type, int screen, int container, int x, int y) {
- return addItem(type, screen, container, x, y, mDefaultProfileId);
+ return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE);
+ }
+
+ public int addItem(int type, int screen, int container, int x, int y, long profileId) {
+ return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE);
+ }
+
+ public int addItem(int type, int screen, int container, int x, int y, String packageName) {
+ return addItem(type, screen, container, x, y, mDefaultProfileId, packageName);
+ }
+
+ public int addItem(int type, int screen, int container, int x, int y, String packageName,
+ int id, Uri contentUri) {
+ addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri);
+ return id;
}
/**
@@ -238,11 +252,19 @@
* @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for
* folder (where the type represents the number of items in the folder).
*/
- public int addItem(int type, int screen, int container, int x, int y, long profileId) {
+ public int addItem(int type, int screen, int container, int x, int y, long profileId,
+ String packageName) {
Context context = RuntimeEnvironment.application;
int id = LauncherSettings.Settings.call(context.getContentResolver(),
LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
+ addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI);
+ return id;
+ }
+
+ public void addItem(int type, int screen, int container, int x, int y, long profileId,
+ String packageName, int id, Uri contentUri) {
+ Context context = RuntimeEnvironment.application;
ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites._ID, id);
@@ -257,7 +279,7 @@
if (type == APP_ICON || type == SHORTCUT) {
values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
values.put(LauncherSettings.Favorites.INTENT,
- new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0));
+ new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0));
} else {
values.put(LauncherSettings.Favorites.ITEM_TYPE,
LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
@@ -267,8 +289,7 @@
}
}
- context.getContentResolver().insert(CONTENT_URI, values);
- return id;
+ context.getContentResolver().insert(contentUri, values);
}
public int[][][] createGrid(int[][][] typeArray) {
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 697048a..a699c32 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -17,6 +17,7 @@
package com.android.launcher3;
import static com.android.launcher3.config.FeatureFlags.MULTI_DB_GRID_MIRATION_ALGO;
+import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -164,8 +165,11 @@
return false;
}
- mOpenHelper.close();
+ DatabaseHelper oldHelper = mOpenHelper;
mOpenHelper = new DatabaseHelper(getContext());
+ copyTable(oldHelper.getReadableDatabase(), Favorites.TABLE_NAME,
+ mOpenHelper.getWritableDatabase(), Favorites.TMP_TABLE, getContext());
+ oldHelper.close();
return true;
}
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 216c221..f516446 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -103,6 +103,11 @@
public static final String PREVIEW_TABLE_NAME = "favorites_preview";
/**
+ * Temporary table used specifically for multi-db grid migrations
+ */
+ public static final String TMP_TABLE = "favorites_tmp";
+
+ /**
* The content:// style URL for "favorites" table
*/
public static final Uri CONTENT_URI = Uri.parse("content://"
@@ -115,6 +120,12 @@
+ LauncherProvider.AUTHORITY + "/" + PREVIEW_TABLE_NAME);
/**
+ * The content:// style URL for "favorites_tmp" table
+ */
+ public static final Uri TMP_CONTENT_URI = Uri.parse("content://"
+ + LauncherProvider.AUTHORITY + "/" + TMP_TABLE);
+
+ /**
* The content:// style URL for a given row, identified by its id.
*
* @param id The row id.
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java
index 3ba740d..a084600 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTask.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java
@@ -936,8 +936,8 @@
boolean dbChanged = false;
if (migrateForPreview) {
- copyTable(transaction.getDb(), Favorites.TABLE_NAME, Favorites.PREVIEW_TABLE_NAME,
- context);
+ copyTable(transaction.getDb(), Favorites.TABLE_NAME, transaction.getDb(),
+ Favorites.PREVIEW_TABLE_NAME, context);
}
GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
@@ -950,10 +950,11 @@
HashSet<String> validPackages = getValidPackages(context);
// Hotseat.
- if (srcHotseatCount != idp.numHotseatIcons) {
- // Migrate hotseat.
- dbChanged = new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
- migrateForPreview, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
+ if (srcHotseatCount != idp.numHotseatIcons
+ && new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
+ migrateForPreview, srcHotseatCount,
+ idp.numHotseatIcons).migrateHotseat()) {
+ dbChanged = true;
}
// Grid size
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 197b29c..0bdccfa 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -16,9 +16,46 @@
package com.android.launcher3.model;
+import static com.android.launcher3.Utilities.getPointString;
+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.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Point;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.LauncherPreviewRenderer;
+import com.android.launcher3.pm.InstallSessionHelper;
+import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
+import com.android.launcher3.util.GridOccupancy;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.widget.WidgetManagerHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* This class takes care of shrinking the workspace (by maximum of one row and one column), as a
@@ -26,14 +63,63 @@
*/
public class GridSizeMigrationTaskV2 {
- private GridSizeMigrationTaskV2(Context context) {
+ public static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
+ public static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";
+ private static final String TAG = "GridSizeMigrationTaskV2";
+ private static final boolean DEBUG = true;
+
+ private final Context mContext;
+ private final SQLiteDatabase mDb;
+ private final DbReader mSrcReader;
+ private final DbReader mDestReader;
+
+ private final List<DbEntry> mHotseatItems;
+ private final List<DbEntry> mWorkspaceItems;
+
+ private final List<DbEntry> mHotseatDiff;
+ private final List<DbEntry> mWorkspaceDiff;
+
+ private final int mDestHotseatSize;
+ private final int mTrgX, mTrgY;
+
+ @VisibleForTesting
+ protected GridSizeMigrationTaskV2(Context context, SQLiteDatabase db, DbReader srcReader,
+ DbReader destReader, int destHotseatSize, Point targetSize) {
+ mContext = context;
+ mDb = db;
+ mSrcReader = srcReader;
+ mDestReader = destReader;
+
+ mHotseatItems = destReader.loadHotseatEntries();
+ mWorkspaceItems = destReader.loadAllWorkspaceEntries();
+
+ mHotseatDiff = calcDiff(mSrcReader.loadHotseatEntries(), mHotseatItems);
+ mWorkspaceDiff = calcDiff(mSrcReader.loadAllWorkspaceEntries(), mWorkspaceItems);
+ mDestHotseatSize = destHotseatSize;
+
+ mTrgX = targetSize.x;
+ mTrgY = targetSize.y;
+ }
+
+ /**
+ * Check given a new IDP, if migration is necessary.
+ */
+ public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
+ SharedPreferences prefs = Utilities.getPrefs(context);
+ String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+ return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+ || idp.numHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+ idp.numHotseatIcons);
}
/** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
public static boolean migrateGridIfNeeded(Context context) {
- // To be implemented.
- return true;
+ if (context instanceof LauncherPreviewRenderer.PreviewContext) {
+ return true;
+ }
+ return migrateGridIfNeeded(context, null);
}
/**
@@ -43,7 +129,608 @@
* @return false if the migration failed.
*/
public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
- // To be implemented.
+ boolean migrateForPreview = idp != null;
+ if (!migrateForPreview) {
+ idp = LauncherAppState.getIDP(context);
+ }
+
+ if (!needsToMigrate(context, idp)) {
+ return true;
+ }
+
+ SharedPreferences prefs = Utilities.getPrefs(context);
+ String gridSizeString = getPointString(idp.numColumns, idp.numRows);
+
+ if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
+ && idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
+ idp.numHotseatIcons)) {
+ // Skip if workspace and hotseat sizes have not changed.
+ return true;
+ }
+
+ HashSet<String> validPackages = getValidPackages(context);
+ int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons);
+
+ if (!LauncherSettings.Settings.call(
+ context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER).getBoolean(
+ LauncherSettings.Settings.EXTRA_VALUE)) {
+ return false;
+ }
+
+ long migrationStartTime = System.currentTimeMillis();
+ try (SQLiteTransaction t = (SQLiteTransaction) LauncherSettings.Settings.call(
+ context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_TRANSACTION).getBinder(
+ LauncherSettings.Settings.EXTRA_VALUE)) {
+
+ DbReader srcReader = new DbReader(t.getDb(), LauncherSettings.Favorites.TMP_TABLE,
+ context, validPackages, srcHotseatCount);
+ DbReader destReader = new DbReader(t.getDb(), LauncherSettings.Favorites.TABLE_NAME,
+ context, validPackages, idp.numHotseatIcons);
+
+ Point targetSize = new Point(idp.numColumns, idp.numRows);
+ GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(),
+ srcReader, destReader, idp.numHotseatIcons, targetSize);
+ task.migrate();
+
+ dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
+
+ t.commit();
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Error during grid migration", e);
+
+ return false;
+ } finally {
+ Log.v(TAG, "Workspace migration completed in "
+ + (System.currentTimeMillis() - migrationStartTime));
+
+ // Save current configuration, so that the migration does not run again.
+ prefs.edit()
+ .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
+ .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
+ .apply();
+ }
+ }
+
+ @VisibleForTesting
+ protected boolean migrate() {
+ if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) {
+ return false;
+ }
+
+ // Migrate hotseat
+ HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader,
+ mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff);
+ hotseatSolution.find();
+
+ // Sort the items by the reading order.
+ Collections.sort(mWorkspaceDiff);
+
+ // Migrate workspace.
+ for (int screenId = 0; screenId <= mDestReader.mLastScreenId; screenId++) {
+ if (DEBUG) {
+ Log.d(TAG, "Migrating " + screenId);
+ }
+ List<DbEntry> entries = mDestReader.loadWorkspaceEntries(screenId);
+ GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+ mContext, entries, screenId, mTrgX, mTrgY, mWorkspaceDiff);
+ workspaceSolution.find();
+ if (mWorkspaceDiff.isEmpty()) {
+ break;
+ }
+ }
+
+ int screenId = mDestReader.mLastScreenId + 1;
+ while (!mWorkspaceDiff.isEmpty()) {
+ GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
+ mContext, new ArrayList<>(), screenId, mTrgX, mTrgY, mWorkspaceDiff);
+ workspaceSolution.find();
+ 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) {
+ Set<String> destSet = dest.parallelStream().map(DbEntry::getIntentStr).collect(
+ Collectors.toSet());
+ List<DbEntry> diff = new ArrayList<>();
+ for (DbEntry entry : src) {
+ if (!destSet.contains(entry.mIntent)) {
+ diff.add(entry);
+ }
+ }
+ return diff;
+ }
+
+ private static void insertEntryInDb(SQLiteDatabase db, Context context,
+ ArrayList<DbEntry> entriesFromSrcDb, DbEntry entry) {
+ int id = -1;
+ switch (entry.itemType) {
+ case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+ for (DbEntry e : entriesFromSrcDb) {
+ if (TextUtils.equals(e.mIntent, entry.mIntent)) {
+ id = e.id;
+ }
+ }
+
+ break;
+ }
+ case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+ for (DbEntry e : entriesFromSrcDb) {
+ if (e.mFolderItems.size() == entry.mFolderItems.size()
+ && e.mFolderItems.containsAll(entry.mFolderItems)) {
+ id = e.id;
+ }
+ }
+ break;
+ }
+ case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
+ case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
+ for (DbEntry e : entriesFromSrcDb) {
+ if (TextUtils.equals(e.mProvider, entry.mProvider)) {
+ id = e.id;
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ return;
+ }
+
+ Cursor c = db.query(LauncherSettings.Favorites.TMP_TABLE, null,
+ LauncherSettings.Favorites._ID + " = '" + id + "'", null, null, null, null);
+
+ while (c.moveToNext()) {
+ ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(c, values);
+ entry.updateContentValues(values);
+ values.put(LauncherSettings.Favorites._ID,
+ LauncherSettings.Settings.call(context.getContentResolver(),
+ LauncherSettings.Settings.METHOD_NEW_ITEM_ID).getInt(
+ LauncherSettings.Settings.EXTRA_VALUE));
+ db.insert(LauncherSettings.Favorites.TABLE_NAME, null, values);
+ }
+ c.close();
+ }
+
+ private static void removeEntryFromDb(SQLiteDatabase db, IntArray entryId) {
+ db.delete(LauncherSettings.Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
+ LauncherSettings.Favorites._ID, entryId), null);
+ }
+
+ private static HashSet<String> getValidPackages(Context context) {
+ // Initialize list of valid packages. This contain all the packages which are already on
+ // the device and packages which are being installed. Any item which doesn't belong to
+ // this set is removed.
+ // Since the loader removes such items anyway, removing these items here doesn't cause
+ // any extra data loss and gives us more free space on the grid for better migration.
+ HashSet<String> validPackages = new HashSet<>();
+ for (PackageInfo info : context.getPackageManager()
+ .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
+ validPackages.add(info.packageName);
+ }
+ InstallSessionHelper.INSTANCE.get(context)
+ .getActiveSessions().keySet()
+ .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
+ return validPackages;
+ }
+
+ protected static class GridPlacementSolution {
+
+ private final SQLiteDatabase mDb;
+ private final DbReader mSrcReader;
+ private final Context mContext;
+ private final GridOccupancy mOccupied;
+ private final int mScreenId;
+ private final int mTrgX;
+ private final int mTrgY;
+ private final List<DbEntry> mItemsToPlace;
+
+ private int mNextStartX;
+ private int mNextStartY;
+
+ GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context,
+ List<DbEntry> placedWorkspaceItems, int screenId, int trgX,
+ int trgY, List<DbEntry> itemsToPlace) {
+ mDb = db;
+ mSrcReader = srcReader;
+ mContext = context;
+ mOccupied = new GridOccupancy(trgX, trgY);
+ mScreenId = screenId;
+ mTrgX = trgX;
+ mTrgY = trgY;
+ mNextStartX = 0;
+ mNextStartY = mTrgY - 1;
+ for (DbEntry entry : placedWorkspaceItems) {
+ mOccupied.markCells(entry, true);
+ }
+ mItemsToPlace = itemsToPlace;
+ }
+
+ public void find() {
+ Iterator<DbEntry> iterator = mItemsToPlace.iterator();
+ while (iterator.hasNext()) {
+ final DbEntry entry = iterator.next();
+ if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
+ iterator.remove();
+ continue;
+ }
+ if (findPlacement(entry)) {
+ insertEntryInDb(mDb, mContext, mSrcReader.mWorkspaceEntries, entry);
+ iterator.remove();
+ }
+ }
+ }
+
+ private boolean findPlacement(DbEntry entry) {
+ for (int y = mNextStartY; y >= 0; y--) {
+ for (int x = mNextStartX; x < mTrgX; x++) {
+ boolean fits = mOccupied.isRegionVacant(x, y, entry.spanX, entry.spanY);
+ boolean minFits = mOccupied.isRegionVacant(x, y, entry.minSpanX,
+ entry.minSpanY);
+ if (minFits) {
+ entry.spanX = entry.minSpanX;
+ entry.spanY = entry.minSpanY;
+ }
+ if (fits || minFits) {
+ entry.screenId = mScreenId;
+ entry.cellX = x;
+ entry.cellY = y;
+ mOccupied.markCells(entry, true);
+ mNextStartX = x + entry.spanX;
+ mNextStartY = y;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ protected static class HotseatPlacementSolution {
+
+ private final SQLiteDatabase mDb;
+ private final DbReader mSrcReader;
+ private final Context mContext;
+ private final HotseatOccupancy mOccupied;
+ private final List<DbEntry> mItemsToPlace;
+
+ HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context,
+ int hotseatSize, List<DbEntry> placedHotseatItems, List<DbEntry> itemsToPlace) {
+ mDb = db;
+ mSrcReader = srcReader;
+ mContext = context;
+ mOccupied = new HotseatOccupancy(hotseatSize);
+ for (DbEntry entry : placedHotseatItems) {
+ mOccupied.markCells(entry, true);
+ }
+ mItemsToPlace = itemsToPlace;
+ }
+
+ public void find() {
+ for (int i = 0; i < mOccupied.mCells.length; i++) {
+ if (!mOccupied.mCells[i] && !mItemsToPlace.isEmpty()) {
+ DbEntry entry = mItemsToPlace.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;
+ insertEntryInDb(mDb, mContext, mSrcReader.mHotseatEntries, entry);
+ mOccupied.markCells(entry, true);
+ }
+ }
+ }
+
+ private class HotseatOccupancy {
+
+ private final boolean[] mCells;
+
+ private HotseatOccupancy(int hotseatSize) {
+ mCells = new boolean[hotseatSize];
+ }
+
+ private void markCells(ItemInfo item, boolean value) {
+ mCells[item.screenId] = value;
+ }
+ }
+ }
+
+ protected static class DbReader {
+
+ private final SQLiteDatabase mDb;
+ private final String mTableName;
+ private final Context mContext;
+ private final HashSet<String> mValidPackages;
+ private final int mHotseatSize;
+ private int mLastScreenId = -1;
+
+ private final ArrayList<DbEntry> mHotseatEntries = new ArrayList<>();
+ private final ArrayList<DbEntry> mWorkspaceEntries = new ArrayList<>();
+
+ DbReader(SQLiteDatabase db, String tableName, Context context,
+ HashSet<String> validPackages, int hotseatSize) {
+ mDb = db;
+ mTableName = tableName;
+ mContext = context;
+ mValidPackages = validPackages;
+ mHotseatSize = hotseatSize;
+ }
+
+ protected ArrayList<DbEntry> loadHotseatEntries() {
+ Cursor c = queryWorkspace(
+ new String[]{
+ LauncherSettings.Favorites._ID, // 0
+ LauncherSettings.Favorites.ITEM_TYPE, // 1
+ LauncherSettings.Favorites.INTENT, // 2
+ LauncherSettings.Favorites.SCREEN}, // 3
+ LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+
+ final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+ final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+ final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+ final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+
+ IntArray entriesToRemove = new IntArray();
+ while (c.moveToNext()) {
+ DbEntry entry = new DbEntry();
+ entry.id = c.getInt(indexId);
+ entry.itemType = c.getInt(indexItemType);
+ entry.screenId = c.getInt(indexScreen);
+
+ if (entry.screenId >= mHotseatSize) {
+ entriesToRemove.add(entry.id);
+ continue;
+ }
+
+ try {
+ // calculate weight
+ switch (entry.itemType) {
+ case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+ entry.mIntent = c.getString(indexIntent);
+ verifyIntent(c.getString(indexIntent));
+ break;
+ }
+ case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+ int total = getFolderItemsCount(entry);
+ if (total == 0) {
+ throw new Exception("Folder is empty");
+ }
+ break;
+ }
+ default:
+ throw new Exception("Invalid item type");
+ }
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.d(TAG, "Removing item " + entry.id, e);
+ }
+ entriesToRemove.add(entry.id);
+ continue;
+ }
+ mHotseatEntries.add(entry);
+ }
+ removeEntryFromDb(mDb, entriesToRemove);
+ c.close();
+ return mHotseatEntries;
+ }
+
+ protected ArrayList<DbEntry> loadAllWorkspaceEntries() {
+ Cursor c = queryWorkspace(
+ new String[]{
+ LauncherSettings.Favorites._ID, // 0
+ LauncherSettings.Favorites.ITEM_TYPE, // 1
+ LauncherSettings.Favorites.SCREEN, // 2
+ LauncherSettings.Favorites.CELLX, // 3
+ LauncherSettings.Favorites.CELLY, // 4
+ LauncherSettings.Favorites.SPANX, // 5
+ LauncherSettings.Favorites.SPANY, // 6
+ LauncherSettings.Favorites.INTENT, // 7
+ LauncherSettings.Favorites.APPWIDGET_PROVIDER, // 8
+ LauncherSettings.Favorites.APPWIDGET_ID}, // 9
+ LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_DESKTOP);
+ return loadWorkspaceEntries(c);
+ }
+
+ protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
+ Cursor c = queryWorkspace(
+ new String[]{
+ LauncherSettings.Favorites._ID, // 0
+ LauncherSettings.Favorites.ITEM_TYPE, // 1
+ LauncherSettings.Favorites.SCREEN, // 2
+ LauncherSettings.Favorites.CELLX, // 3
+ LauncherSettings.Favorites.CELLY, // 4
+ LauncherSettings.Favorites.SPANX, // 5
+ LauncherSettings.Favorites.SPANY, // 6
+ LauncherSettings.Favorites.INTENT, // 7
+ LauncherSettings.Favorites.APPWIDGET_PROVIDER, // 8
+ LauncherSettings.Favorites.APPWIDGET_ID}, // 9
+ LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_DESKTOP
+ + " AND " + LauncherSettings.Favorites.SCREEN + " = " + screen);
+ return loadWorkspaceEntries(c);
+ }
+
+ private ArrayList<DbEntry> loadWorkspaceEntries(Cursor c) {
+ final int indexId = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
+ final int indexItemType = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
+ final int indexScreen = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
+ final int indexCellX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
+ final int indexCellY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+ final int indexSpanX = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX);
+ final int indexSpanY = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY);
+ final int indexIntent = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
+ final int indexAppWidgetProvider = c.getColumnIndexOrThrow(
+ LauncherSettings.Favorites.APPWIDGET_PROVIDER);
+ final int indexAppWidgetId = c.getColumnIndexOrThrow(
+ LauncherSettings.Favorites.APPWIDGET_ID);
+
+ IntArray entriesToRemove = new IntArray();
+ WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
+ while (c.moveToNext()) {
+ DbEntry entry = new DbEntry();
+ entry.id = c.getInt(indexId);
+ entry.itemType = c.getInt(indexItemType);
+ entry.screenId = c.getInt(indexScreen);
+ mLastScreenId = Math.max(mLastScreenId, entry.screenId);
+ entry.cellX = c.getInt(indexCellX);
+ entry.cellY = c.getInt(indexCellY);
+ entry.spanX = c.getInt(indexSpanX);
+ entry.spanY = c.getInt(indexSpanY);
+
+ try {
+ // calculate weight
+ switch (entry.itemType) {
+ case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
+ case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: {
+ entry.mIntent = c.getString(indexIntent);
+ verifyIntent(entry.mIntent);
+ break;
+ }
+ case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: {
+ entry.mProvider = c.getString(indexAppWidgetProvider);
+ ComponentName cn = ComponentName.unflattenFromString(entry.mProvider);
+ verifyPackage(cn.getPackageName());
+
+ int widgetId = c.getInt(indexAppWidgetId);
+ LauncherAppWidgetProviderInfo pInfo =
+ widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
+ Point spans = null;
+ if (pInfo != null) {
+ spans = pInfo.getMinSpans();
+ }
+ if (spans != null) {
+ entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
+ entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
+ } else {
+ // Assume that the widget be resized down to 2x2
+ entry.minSpanX = entry.minSpanY = 2;
+ }
+
+ break;
+ }
+ case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
+ int total = getFolderItemsCount(entry);
+ if (total == 0) {
+ throw new Exception("Folder is empty");
+ }
+ break;
+ }
+ default:
+ throw new Exception("Invalid item type");
+ }
+ } catch (Exception e) {
+ if (DEBUG) {
+ Log.d(TAG, "Removing item " + entry.id, e);
+ }
+ entriesToRemove.add(entry.id);
+ continue;
+ }
+ mWorkspaceEntries.add(entry);
+ }
+ removeEntryFromDb(mDb, entriesToRemove);
+ c.close();
+ return mWorkspaceEntries;
+ }
+
+ private int getFolderItemsCount(DbEntry entry) {
+ Cursor c = queryWorkspace(
+ new String[]{LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT},
+ LauncherSettings.Favorites.CONTAINER + " = " + entry.id);
+
+ int total = 0;
+ while (c.moveToNext()) {
+ try {
+ String intent = c.getString(1);
+ verifyIntent(intent);
+ total++;
+ entry.mFolderItems.add(intent);
+ } catch (Exception e) {
+ removeEntryFromDb(mDb, IntArray.wrap(c.getInt(0)));
+ }
+ }
+ c.close();
+ return total;
+ }
+
+ private Cursor queryWorkspace(String[] columns, String where) {
+ return mDb.query(mTableName, columns, where, null, null, null, null);
+ }
+
+ /** Verifies if the mIntent should be restored. */
+ private void verifyIntent(String intentStr)
+ throws Exception {
+ Intent intent = Intent.parseUri(intentStr, 0);
+ if (intent.getComponent() != null) {
+ verifyPackage(intent.getComponent().getPackageName());
+ } else if (intent.getPackage() != null) {
+ // Only verify package if the component was null.
+ verifyPackage(intent.getPackage());
+ }
+ }
+
+ /** Verifies if the package should be restored */
+ private void verifyPackage(String packageName)
+ throws Exception {
+ if (!mValidPackages.contains(packageName)) {
+ // TODO(b/151468819): Handle promise app icon restoration during grid migration.
+ throw new Exception("Package not available");
+ }
+ }
+ }
+
+ protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
+
+ private String mIntent;
+ private String mProvider;
+ private Set<String> mFolderItems = new HashSet<>();
+
+ /** 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(mIntent, entry.mIntent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIntent);
+ }
+
+ 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);
+ }
+
+ public String getIntentStr() {
+ return mIntent;
+ }
+ }
}
diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java
index f7ecc3f..dacea84 100644
--- a/src/com/android/launcher3/provider/LauncherDbUtils.java
+++ b/src/com/android/launcher3/provider/LauncherDbUtils.java
@@ -118,13 +118,20 @@
db.execSQL("DROP TABLE IF EXISTS " + tableName);
}
- /** Copy from table to the to table. */
- public static void copyTable(SQLiteDatabase db, String from, String to, Context context) {
+ /** Copy fromTable in fromDb to toTable in toDb. */
+ public static void copyTable(SQLiteDatabase fromDb, String fromTable, SQLiteDatabase toDb,
+ String toTable, Context context) {
long userSerial = UserCache.INSTANCE.get(context).getSerialNumberForUser(
Process.myUserHandle());
- dropTable(db, to);
- Favorites.addTableToDb(db, userSerial, false, to);
- db.execSQL("INSERT INTO " + to + " SELECT * FROM " + from);
+ dropTable(toDb, toTable);
+ Favorites.addTableToDb(toDb, userSerial, false, toTable);
+ if (fromDb != toDb) {
+ toDb.execSQL("ATTACH DATABASE '" + fromDb.getPath() + "' AS from_db");
+ toDb.execSQL(
+ "INSERT INTO " + toTable + " SELECT * FROM from_db." + fromTable);
+ } else {
+ toDb.execSQL("INSERT INTO " + toTable + " SELECT * FROM " + fromTable);
+ }
}
/**