From f862a26347b583bd84be22a8ceff4bc13158ec7e Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Mon, 14 Dec 2015 14:27:38 -0800 Subject: [PATCH] Extending the grid migration logic to handle density changes For hotseat migratino, we simply drop the items with least weight If the workspace row/column decreases by 2 or more, we clear the whole workspace Bug: 25958224 Change-Id: I7131b955023d185ed10955f593184b9238546dc8 --- .../launcher3/InvariantDeviceProfile.java | 4 +- .../launcher3/LauncherBackupAgentHelper.java | 7 +- .../launcher3/LauncherBackupHelper.java | 4 +- src/com/android/launcher3/LauncherModel.java | 10 +- ...reTask.java => GridSizeMigrationTask.java} | 260 +++++++++++++++--- .../android/launcher3/util/ConfigMonitor.java | 12 +- 6 files changed, 240 insertions(+), 57 deletions(-) rename src/com/android/launcher3/model/{MigrateFromRestoreTask.java => GridSizeMigrationTask.java} (78%) diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java index a91181d5e3..56c0192f6b 100644 --- a/src/com/android/launcher3/InvariantDeviceProfile.java +++ b/src/com/android/launcher3/InvariantDeviceProfile.java @@ -73,12 +73,12 @@ public class InvariantDeviceProfile { /** * Number of icons inside the hotseat area. */ - int numHotseatIcons; + public int numHotseatIcons; float hotseatIconSize; int defaultLayoutId; // Derived invariant properties - int hotseatAllAppsRank; + public int hotseatAllAppsRank; DeviceProfile landscapeProfile; DeviceProfile portraitProfile; diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index 0773bf27c5..bf9c668221 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -24,7 +24,7 @@ import android.database.Cursor; import android.os.ParcelFileDescriptor; import android.util.Log; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import java.io.IOException; @@ -101,8 +101,9 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { LauncherSettings.Settings.METHOD_UPDATE_FOLDER_ITEMS_RANK); } - if (MigrateFromRestoreTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) { - MigrateFromRestoreTask.markForMigration(getApplicationContext(), + // TODO: Update this logic to handle grid difference of 2. as well as hotseat difference + if (GridSizeMigrationTask.ENABLED && mHelper.shouldAttemptWorkspaceMigration()) { + GridSizeMigrationTask.markForMigration(getApplicationContext(), (int) mHelper.migrationCompatibleProfileData.desktopCols, (int) mHelper.migrationCompatibleProfileData.desktopRows, mHelper.widgetSizes); diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index 509fbf8b88..4ebead56a1 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -52,7 +52,7 @@ import com.android.launcher3.backup.BackupProtos.Screen; import com.android.launcher3.backup.BackupProtos.Widget; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.util.Thunk; import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; @@ -315,7 +315,7 @@ public class LauncherBackupHelper implements BackupHelper { return true; } - if (MigrateFromRestoreTask.ENABLED && + if (GridSizeMigrationTask.ENABLED && (oldProfile.desktopCols - currentProfile.desktopCols <= 1) && (oldProfile.desktopRows - currentProfile.desktopRows <= 1)) { // Allow desktop migration when row and/or column count contracts by 1. diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java index fe0abc0244..0eb1a90b05 100644 --- a/src/com/android/launcher3/LauncherModel.java +++ b/src/com/android/launcher3/LauncherModel.java @@ -57,7 +57,7 @@ import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.config.ProviderConfig; -import com.android.launcher3.model.MigrateFromRestoreTask; +import com.android.launcher3.model.GridSizeMigrationTask; import com.android.launcher3.model.WidgetsModel; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.CursorIconInfo; @@ -1651,14 +1651,14 @@ public class LauncherModel extends BroadcastReceiver int countX = profile.numColumns; int countY = profile.numRows; - if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) { + if (GridSizeMigrationTask.ENABLED && GridSizeMigrationTask.shouldRunTask(mContext)) { long migrationStartTime = System.currentTimeMillis(); Log.v(TAG, "Starting workspace migration after restore"); try { - MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext); + GridSizeMigrationTask task = new GridSizeMigrationTask(mContext); // Clear the flags before starting the task, so that we do not run the task // again, in case there was an uncaught error. - MigrateFromRestoreTask.clearFlags(mContext); + GridSizeMigrationTask.clearFlags(mContext); task.execute(); } catch (Exception e) { Log.e(TAG, "Error during grid migration", e); @@ -1668,6 +1668,8 @@ public class LauncherModel extends BroadcastReceiver } Log.v(TAG, "Workspace migration completed in " + (System.currentTimeMillis() - migrationStartTime)); + + GridSizeMigrationTask.saveCurrentConfig(mContext); } if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) { diff --git a/src/com/android/launcher3/model/MigrateFromRestoreTask.java b/src/com/android/launcher3/model/GridSizeMigrationTask.java similarity index 78% rename from src/com/android/launcher3/model/MigrateFromRestoreTask.java rename to src/com/android/launcher3/model/GridSizeMigrationTask.java index 9cabc8d413..08c3dc0bbe 100644 --- a/src/com/android/launcher3/model/MigrateFromRestoreTask.java +++ b/src/com/android/launcher3/model/GridSizeMigrationTask.java @@ -24,29 +24,33 @@ import com.android.launcher3.Utilities; import com.android.launcher3.compat.PackageInstallerCompat; import com.android.launcher3.compat.UserHandleCompat; import com.android.launcher3.util.LongArrayMap; -import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Locale; /** * 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. + * result of restoring from a larger device or device density change. */ -public class MigrateFromRestoreTask { +public class GridSizeMigrationTask { - public static boolean ENABLED = false; + public static boolean ENABLED = Utilities.isNycOrAbove(); - private static final String TAG = "MigrateFromRestoreTask"; + private static final String TAG = "GridSizeMigrationTask"; private static final boolean DEBUG = true; - private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size"; + private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size"; + private static final String KEY_MIGRATION_SRC_HOTSEAT_SIZE = "migration_src_hotseat_size"; + + // Set of entries indicating minimum size a widget can be resized to. This is used during + // restore in case the widget has not been installed yet. private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size"; // These are carefully selected weights for various item types (Math.random?), to allow for - // the lease absurd migration experience. + // the least absurd migration experience. private static final float WT_SHORTCUT = 1; private static final float WT_APPLICATION = 0.8f; private static final float WT_WIDGET_MIN = 2; @@ -65,17 +69,37 @@ public class MigrateFromRestoreTask { private ArrayList mCarryOver; private final int mSrcX, mSrcY; - @Thunk final int mTrgX, mTrgY; + private final int mTrgX, mTrgY; private final boolean mShouldRemoveX, mShouldRemoveY; - public MigrateFromRestoreTask(Context context) { + private final int mSrcHotseatSize; + private final int mSrcAllAppsRank; + + /** + * TODO: Create a generic constructor which can be unit tested. + */ + public GridSizeMigrationTask(Context context) { mContext = context; + + mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + mTrgX = mIdp.numColumns; + mTrgY = mIdp.numRows; + SharedPreferences prefs = Utilities.getPrefs(context); - Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, "")); + Point sourceSize = parsePoint( + prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(mTrgX, mTrgY))); mSrcX = sourceSize.x; mSrcY = sourceSize.y; + // Hotseat + Point hotseatSize = parsePoint( + prefs.getString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, + getPointString(mIdp.numHotseatIcons, mIdp.hotseatAllAppsRank))); + mSrcHotseatSize = hotseatSize.x; + mSrcAllAppsRank = hotseatSize.y; + + // Widget sizes mWidgetMinSize = new HashMap(); for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE, Collections.emptySet())) { @@ -83,16 +107,12 @@ public class MigrateFromRestoreTask { mWidgetMinSize.put(parts[0], parsePoint(parts[1])); } - mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile(); - mTrgX = mIdp.numColumns; - mTrgY = mIdp.numRows; mShouldRemoveX = mTrgX < mSrcX; mShouldRemoveY = mTrgY < mSrcY; } public void execute() throws Exception { mEntryToRemove = new ArrayList<>(); - mCarryOver = new ArrayList<>(); mUpdateOperations = new ArrayList<>(); // Initialize list of valid packages. This contain all the packages which are already on @@ -107,6 +127,97 @@ public class MigrateFromRestoreTask { mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext) .updateAndGetActiveSessionCache().keySet()); + // Migrate hotseat + if (mSrcHotseatSize != mIdp.numHotseatIcons || mSrcAllAppsRank != mIdp.hotseatAllAppsRank) { + migrateHotseat(); + } + + if (mShouldRemoveX || mShouldRemoveY) { + if ((mSrcY - mTrgX) > 1 || (mSrcY - mSrcY) > 1) { + // TODO: support this. + throw new Exception("The universe is too large for migration"); + } else { + migrateWorkspace(); + } + } + + // Update items + if (!mUpdateOperations.isEmpty()) { + mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); + } + + if (!mEntryToRemove.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); + } + mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, + Utilities.createDbSelectionQuery( + LauncherSettings.Favorites._ID, mEntryToRemove), null); + } + + if (!mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty()) { + // Make sure we haven't removed everything. + final Cursor c = mContext.getContentResolver().query( + LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); + boolean hasData = c.moveToNext(); + c.close(); + if (!hasData) { + throw new Exception("Removed every thing during grid resize"); + } + } + } + + /** + * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them + * in the order in the new hotseat while keeping an empty space for all-apps. If the number of + * entries is more than what can fit in the new hotseat, we drop the entries with least weight. + * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION} + * & {@see #WT_FOLDER_FACTOR}. + */ + private void migrateHotseat() { + ArrayList items = loadHotseatEntries(); + + int requiredCount = mIdp.numHotseatIcons - 1; + + while (items.size() > requiredCount) { + // Pick the center item by default. + DbEntry toRemove = items.get(items.size() / 2); + + // Find the item with least weight. + for (DbEntry entry : items) { + if (entry.weight < toRemove.weight) { + toRemove = entry; + } + } + + mEntryToRemove.add(toRemove.id); + items.remove(toRemove); + } + + // Update screen IDS + int newScreenId = 0; + for (DbEntry entry : items) { + if (entry.screenId != newScreenId) { + entry.screenId = newScreenId; + + // These values does not affect the item position, but we should set them + // to something other than -1. + entry.cellX = newScreenId; + entry.cellY = 0; + + update(entry); + } + + newScreenId++; + if (newScreenId == mIdp.hotseatAllAppsRank) { + newScreenId++; + } + } + } + + private void migrateWorkspace() throws Exception { + mCarryOver = new ArrayList<>(); + ArrayList allScreens = LauncherModel.loadWorkspaceScreensDb(mContext); if (allScreens.isEmpty()) { throw new Exception("Unable to get workspace screens"); @@ -157,27 +268,6 @@ public class MigrateFromRestoreTask { LauncherAppState.getInstance().getModel() .updateWorkspaceScreenOrder(mContext, allScreens); } - - // Update items - mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations); - - if (!mEntryToRemove.isEmpty()) { - if (DEBUG) { - Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove)); - } - mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI, - Utilities.createDbSelectionQuery( - LauncherSettings.Favorites._ID, mEntryToRemove), null); - } - - // Make sure we haven't removed everything. - final Cursor c = mContext.getContentResolver().query( - LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); - boolean hasData = c.moveToNext(); - c.close(); - if (!hasData) { - throw new Exception("Removed every thing during grid resize"); - } } /** @@ -191,7 +281,7 @@ public class MigrateFromRestoreTask { * (otherwise they are placed on a new screen). */ private void migrateScreen(long screenId) { - ArrayList items = loadEntries(screenId); + ArrayList items = loadWorkspaceEntries(screenId); int removedCol = Integer.MAX_VALUE; int removedRow = Integer.MAX_VALUE; @@ -329,7 +419,7 @@ public class MigrateFromRestoreTask { return finalItems; } - @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) { + private void markCells(boolean[][] occupied, DbEntry item, boolean val) { for (int i = item.cellX; i < (item.cellX + item.spanX); i++) { for (int j = item.cellY; j < (item.cellY + item.spanY); j++) { occupied[i][j] = val; @@ -337,7 +427,7 @@ public class MigrateFromRestoreTask { } } - @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { + private boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) { if (x + w > mTrgX) return false; if (y + h > mTrgY) return false; @@ -545,10 +635,71 @@ public class MigrateFromRestoreTask { } } + private ArrayList loadHotseatEntries() { + Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, + new String[]{ + Favorites._ID, // 0 + Favorites.ITEM_TYPE, // 1 + Favorites.INTENT, // 2 + Favorites.SCREEN}, // 3 + Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null); + + final int indexId = c.getColumnIndexOrThrow(Favorites._ID); + final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE); + final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT); + final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN); + + ArrayList entries = new ArrayList<>(); + while (c.moveToNext()) { + DbEntry entry = new DbEntry(); + entry.id = c.getLong(indexId); + entry.itemType = c.getInt(indexItemType); + entry.screenId = c.getLong(indexScreen); + + if (entry.screenId >= mSrcHotseatSize) { + mEntryToRemove.add(entry.id); + continue; + } + + try { + // calculate weight + switch (entry.itemType) { + case Favorites.ITEM_TYPE_SHORTCUT: + case Favorites.ITEM_TYPE_APPLICATION: { + verifyIntent(c.getString(indexIntent)); + entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT + ? WT_SHORTCUT : WT_APPLICATION; + break; + } + case Favorites.ITEM_TYPE_FOLDER: { + int total = getFolderItemsCount(entry.id); + if (total == 0) { + throw new Exception("Folder is empty"); + } + entry.weight = WT_FOLDER_FACTOR * total; + break; + } + default: + throw new Exception("Invalid item type"); + } + } catch (Exception e) { + if (DEBUG) { + Log.d(TAG, "Removing item " + entry.id, e); + } + mEntryToRemove.add(entry.id); + continue; + } + entries.add(entry); + } + c.close(); + return entries; + } + + /** * Loads entries for a particular screen id. */ - public ArrayList loadEntries(long screen) { + private ArrayList loadWorkspaceEntries(long screen) { Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI, new String[] { Favorites._ID, // 0 @@ -733,7 +884,7 @@ public class MigrateFromRestoreTask { } } - @Thunk static ArrayList deepCopy(ArrayList src) { + private static ArrayList deepCopy(ArrayList src) { ArrayList dup = new ArrayList(src.size()); for (DbEntry e : src) { dup.add(e.copy()); @@ -749,18 +900,39 @@ public class MigrateFromRestoreTask { public static void markForMigration(Context context, int srcX, int srcY, HashSet widgets) { Utilities.getPrefs(context).edit() - .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY) + .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(srcX, srcY)) .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets) .apply(); } public static boolean shouldRunTask(Context context) { - return !TextUtils.isEmpty(Utilities.getPrefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, "")); + SharedPreferences prefs = Utilities.getPrefs(context); + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + + // Run task if workspace or hotseat size has changed. + return !getPointString(idp.numColumns, idp.numRows).equals( + prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) + || !getPointString(idp.numHotseatIcons, idp.hotseatAllAppsRank).equals( + prefs.getString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, "")); } public static void clearFlags(Context context) { - Utilities.getPrefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE) - .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); + Utilities.getPrefs(context).edit().remove(KEY_MIGRATION_WIDGET_MINSIZE).commit(); + } + + public static void saveCurrentConfig(Context context) { + InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile(); + Utilities.getPrefs(context).edit() + .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, + getPointString(idp.numColumns, idp.numRows)) + .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, + getPointString(idp.numHotseatIcons, idp.hotseatAllAppsRank)) + .remove(KEY_MIGRATION_WIDGET_MINSIZE) + .commit(); + } + + private static String getPointString(int x, int y) { + return String.format(Locale.ENGLISH, "%d,%d", x, y); } } diff --git a/src/com/android/launcher3/util/ConfigMonitor.java b/src/com/android/launcher3/util/ConfigMonitor.java index c61fa88591..bdb16390b2 100644 --- a/src/com/android/launcher3/util/ConfigMonitor.java +++ b/src/com/android/launcher3/util/ConfigMonitor.java @@ -23,26 +23,30 @@ import android.content.IntentFilter; import android.content.res.Configuration; import android.util.Log; +import com.android.launcher3.Utilities; + /** * {@link BroadcastReceiver} which watches configuration changes and - * restarts the process in case changes which affect the device profile. + * restarts the process in case changes which affect the device profile occur. */ public class ConfigMonitor extends BroadcastReceiver { private final Context mContext; private final float mFontScale; + private final int mDensity; public ConfigMonitor(Context context) { mContext = context; Configuration config = context.getResources().getConfiguration(); mFontScale = config.fontScale; + mDensity = getDensity(config); } @Override public void onReceive(Context context, Intent intent) { Configuration config = context.getResources().getConfiguration(); - if (mFontScale != config.fontScale) { + if (mFontScale != config.fontScale || mDensity != getDensity(config)) { Log.d("ConfigMonitor", "Configuration changed, restarting launcher"); mContext.unregisterReceiver(this); android.os.Process.killProcess(android.os.Process.myPid()); @@ -52,4 +56,8 @@ public class ConfigMonitor extends BroadcastReceiver { public void register() { mContext.registerReceiver(this, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); } + + private static int getDensity(Configuration config) { + return Utilities.ATLEAST_JB_MR1 ? config.densityDpi : 0; + } }