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
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index a91181d..56c0192 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -73,12 +73,12 @@
     /**
      * 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 0773bf2..bf9c668 100644
--- a/src/com/android/launcher3/LauncherBackupAgentHelper.java
+++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java
@@ -24,7 +24,7 @@
 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 @@
                         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 509fbf8..4ebead5 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.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 @@
             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 fe0abc0..0eb1a90 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -57,7 +57,7 @@
 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 @@
             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 @@
                 }
                 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 9cabc8d..08c3dc0 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.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 @@
     private ArrayList<DbEntry> 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<String, Point>();
         for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE,
                 Collections.<String>emptySet())) {
@@ -83,16 +107,12 @@
             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 @@
         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<DbEntry> 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<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext);
         if (allScreens.isEmpty()) {
             throw new Exception("Unable to get workspace screens");
@@ -157,27 +268,6 @@
             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 @@
      *      (otherwise they are placed on a new screen).
      */
     private void migrateScreen(long screenId) {
-        ArrayList<DbEntry> items = loadEntries(screenId);
+        ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
 
         int removedCol = Integer.MAX_VALUE;
         int removedRow = Integer.MAX_VALUE;
@@ -329,7 +419,7 @@
         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 @@
         }
     }
 
-    @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 @@
         }
     }
 
+    private ArrayList<DbEntry> 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<DbEntry> 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<DbEntry> loadEntries(long screen) {
+    private ArrayList<DbEntry> loadWorkspaceEntries(long screen) {
        Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
                 new String[] {
                     Favorites._ID,                  // 0
@@ -733,7 +884,7 @@
         }
     }
 
-    @Thunk static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
+    private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
         ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size());
         for (DbEntry e : src) {
             dup.add(e.copy());
@@ -749,18 +900,39 @@
     public static void markForMigration(Context context, int srcX, int srcY,
             HashSet<String> 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 c61fa88..bdb1639 100644
--- a/src/com/android/launcher3/util/ConfigMonitor.java
+++ b/src/com/android/launcher3/util/ConfigMonitor.java
@@ -23,26 +23,30 @@
 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 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;
+    }
 }