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 0000000000..8f58d8b507 --- /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 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 e133cf2778..20b1453b6f 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 class LauncherModelHelper { } 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 @@ public class LauncherModelHelper { * @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 @@ public class LauncherModelHelper { 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 @@ public class LauncherModelHelper { } } - 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 697048a0da..a699c32848 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 @@ public class LauncherProvider extends ContentProvider { 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 216c221dc6..f516446ccb 100644 --- a/src/com/android/launcher3/LauncherSettings.java +++ b/src/com/android/launcher3/LauncherSettings.java @@ -102,6 +102,11 @@ public class LauncherSettings { */ 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 */ @@ -114,6 +119,12 @@ public class LauncherSettings { public static final Uri PREVIEW_CONTENT_URI = Uri.parse("content://" + 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. * diff --git a/src/com/android/launcher3/model/GridSizeMigrationTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java index 3ba740d4d1..a0846001a1 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationTask.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java @@ -936,8 +936,8 @@ public class GridSizeMigrationTask { 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 @@ public class GridSizeMigrationTask { HashSet 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 197b29c688..0bdccfa16c 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 @@ import com.android.launcher3.InvariantDeviceProfile; */ 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 mHotseatItems; + private final List mWorkspaceItems; + + private final List mHotseatDiff; + private final List 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 @@ public class GridSizeMigrationTaskV2 { * @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 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 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 calcDiff(List src, List dest) { + Set destSet = dest.parallelStream().map(DbEntry::getIntentStr).collect( + Collectors.toSet()); + List 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 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 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 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 mItemsToPlace; + + private int mNextStartX; + private int mNextStartY; + + GridPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context, + List placedWorkspaceItems, int screenId, int trgX, + int trgY, List 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 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 mItemsToPlace; + + HotseatPlacementSolution(SQLiteDatabase db, DbReader srcReader, Context context, + int hotseatSize, List placedHotseatItems, List 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 mValidPackages; + private final int mHotseatSize; + private int mLastScreenId = -1; + + private final ArrayList mHotseatEntries = new ArrayList<>(); + private final ArrayList mWorkspaceEntries = new ArrayList<>(); + + DbReader(SQLiteDatabase db, String tableName, Context context, + HashSet validPackages, int hotseatSize) { + mDb = db; + mTableName = tableName; + mContext = context; + mValidPackages = validPackages; + mHotseatSize = hotseatSize; + } + + protected ArrayList 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 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 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 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 { + + private String mIntent; + private String mProvider; + private Set 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 f7ecc3f1d7..dacea84613 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.java +++ b/src/com/android/launcher3/provider/LauncherDbUtils.java @@ -118,13 +118,20 @@ public class LauncherDbUtils { 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); + } } /**