Home | History | Annotate | Download | only in model
      1 package com.android.launcher3.model;
      2 
      3 import android.content.ComponentName;
      4 import android.content.ContentProviderOperation;
      5 import android.content.ContentValues;
      6 import android.content.Context;
      7 import android.content.Intent;
      8 import android.content.SharedPreferences;
      9 import android.content.pm.PackageInfo;
     10 import android.database.Cursor;
     11 import android.graphics.Point;
     12 import android.net.Uri;
     13 import android.text.TextUtils;
     14 import android.util.Log;
     15 
     16 import com.android.launcher3.InvariantDeviceProfile;
     17 import com.android.launcher3.ItemInfo;
     18 import com.android.launcher3.LauncherAppState;
     19 import com.android.launcher3.LauncherAppWidgetProviderInfo;
     20 import com.android.launcher3.LauncherModel;
     21 import com.android.launcher3.LauncherProvider;
     22 import com.android.launcher3.LauncherSettings;
     23 import com.android.launcher3.LauncherSettings.Favorites;
     24 import com.android.launcher3.Utilities;
     25 import com.android.launcher3.backup.nano.BackupProtos;
     26 import com.android.launcher3.compat.AppWidgetManagerCompat;
     27 import com.android.launcher3.compat.PackageInstallerCompat;
     28 import com.android.launcher3.util.LongArrayMap;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Collections;
     32 import java.util.HashMap;
     33 import java.util.HashSet;
     34 import java.util.Locale;
     35 
     36 /**
     37  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
     38  * result of restoring from a larger device or device density change.
     39  */
     40 public class GridSizeMigrationTask {
     41 
     42     public static boolean ENABLED = Utilities.ATLEAST_N;
     43 
     44     private static final String TAG = "GridSizeMigrationTask";
     45     private static final boolean DEBUG = true;
     46 
     47     private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
     48     private static final String KEY_MIGRATION_SRC_HOTSEAT_SIZE = "migration_src_hotseat_size";
     49 
     50     // Set of entries indicating minimum size a widget can be resized to. This is used during
     51     // restore in case the widget has not been installed yet.
     52     private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size";
     53 
     54     // These are carefully selected weights for various item types (Math.random?), to allow for
     55     // the least absurd migration experience.
     56     private static final float WT_SHORTCUT = 1;
     57     private static final float WT_APPLICATION = 0.8f;
     58     private static final float WT_WIDGET_MIN = 2;
     59     private static final float WT_WIDGET_FACTOR = 0.6f;
     60     private static final float WT_FOLDER_FACTOR = 0.5f;
     61 
     62     private final Context mContext;
     63     private final InvariantDeviceProfile mIdp;
     64 
     65     private final HashMap<String, Point> mWidgetMinSize = new HashMap<>();
     66     private final ContentValues mTempValues = new ContentValues();
     67     private final ArrayList<Long> mEntryToRemove = new ArrayList<>();
     68     private final ArrayList<ContentProviderOperation> mUpdateOperations = new ArrayList<>();
     69     private final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
     70     private final HashSet<String> mValidPackages;
     71 
     72     private final int mSrcX, mSrcY;
     73     private final int mTrgX, mTrgY;
     74     private final boolean mShouldRemoveX, mShouldRemoveY;
     75 
     76     private final int mSrcHotseatSize;
     77     private final int mSrcAllAppsRank;
     78     private final int mDestHotseatSize;
     79     private final int mDestAllAppsRank;
     80 
     81     protected GridSizeMigrationTask(Context context, InvariantDeviceProfile idp,
     82             HashSet<String> validPackages, HashMap<String, Point> widgetMinSize,
     83             Point sourceSize, Point targetSize) {
     84         mContext = context;
     85         mValidPackages = validPackages;
     86         mWidgetMinSize.putAll(widgetMinSize);
     87         mIdp = idp;
     88 
     89         mSrcX = sourceSize.x;
     90         mSrcY = sourceSize.y;
     91 
     92         mTrgX = targetSize.x;
     93         mTrgY = targetSize.y;
     94 
     95         mShouldRemoveX = mTrgX < mSrcX;
     96         mShouldRemoveY = mTrgY < mSrcY;
     97 
     98         // Non-used variables
     99         mSrcHotseatSize = mSrcAllAppsRank = mDestHotseatSize = mDestAllAppsRank = -1;
    100     }
    101 
    102     protected GridSizeMigrationTask(Context context,
    103             InvariantDeviceProfile idp, HashSet<String> validPackages,
    104             int srcHotseatSize, int srcAllAppsRank,
    105             int destHotseatSize, int destAllAppsRank) {
    106         mContext = context;
    107         mIdp = idp;
    108         mValidPackages = validPackages;
    109 
    110         mSrcHotseatSize = srcHotseatSize;
    111         mSrcAllAppsRank = srcAllAppsRank;
    112 
    113         mDestHotseatSize = destHotseatSize;
    114         mDestAllAppsRank = destAllAppsRank;
    115 
    116         // Non-used variables
    117         mSrcX = mSrcY = mTrgX = mTrgY = -1;
    118         mShouldRemoveX = mShouldRemoveY = false;
    119     }
    120 
    121     /**
    122      * Applied all the pending DB operations
    123      * @return true if any DB operation was commited.
    124      */
    125     private boolean applyOperations() throws Exception {
    126         // Update items
    127         if (!mUpdateOperations.isEmpty()) {
    128             mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations);
    129         }
    130 
    131         if (!mEntryToRemove.isEmpty()) {
    132             if (DEBUG) {
    133                 Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove));
    134             }
    135             mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
    136                     Utilities.createDbSelectionQuery(
    137                             LauncherSettings.Favorites._ID, mEntryToRemove), null);
    138         }
    139 
    140         return !mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty();
    141     }
    142 
    143     /**
    144      * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them
    145      * in the order in the new hotseat while keeping an empty space for all-apps. If the number of
    146      * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
    147      * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
    148      * & {@see #WT_FOLDER_FACTOR}.
    149      * @return true if any DB change was made
    150      */
    151     protected boolean migrateHotseat() throws Exception {
    152         ArrayList<DbEntry> items = loadHotseatEntries();
    153 
    154         int requiredCount = mDestHotseatSize - 1;
    155 
    156         while (items.size() > requiredCount) {
    157             // Pick the center item by default.
    158             DbEntry toRemove = items.get(items.size() / 2);
    159 
    160             // Find the item with least weight.
    161             for (DbEntry entry : items) {
    162                 if (entry.weight < toRemove.weight) {
    163                     toRemove = entry;
    164                 }
    165             }
    166 
    167             mEntryToRemove.add(toRemove.id);
    168             items.remove(toRemove);
    169         }
    170 
    171         // Update screen IDS
    172         int newScreenId = 0;
    173         for (DbEntry entry : items) {
    174             if (entry.screenId != newScreenId) {
    175                 entry.screenId = newScreenId;
    176 
    177                 // These values does not affect the item position, but we should set them
    178                 // to something other than -1.
    179                 entry.cellX = newScreenId;
    180                 entry.cellY = 0;
    181 
    182                 update(entry);
    183             }
    184 
    185             newScreenId++;
    186             if (newScreenId == mDestAllAppsRank) {
    187                 newScreenId++;
    188             }
    189         }
    190 
    191         return applyOperations();
    192     }
    193 
    194     /**
    195      * @return true if any DB change was made
    196      */
    197     protected boolean migrateWorkspace() throws Exception {
    198         ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext);
    199         if (allScreens.isEmpty()) {
    200             throw new Exception("Unable to get workspace screens");
    201         }
    202 
    203         for (long screenId : allScreens) {
    204             if (DEBUG) {
    205                 Log.d(TAG, "Migrating " + screenId);
    206             }
    207             migrateScreen(screenId);
    208         }
    209 
    210         if (!mCarryOver.isEmpty()) {
    211             LongArrayMap<DbEntry> itemMap = new LongArrayMap<>();
    212             for (DbEntry e : mCarryOver) {
    213                 itemMap.put(e.id, e);
    214             }
    215 
    216             do {
    217                 // Some items are still remaining. Try adding a few new screens.
    218 
    219                 // At every iteration, make sure that at least one item is removed from
    220                 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
    221                 // break the loop and abort migration by throwing an exception.
    222                 OptimalPlacementSolution placement = new OptimalPlacementSolution(
    223                         new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true);
    224                 placement.find();
    225                 if (placement.finalPlacedItems.size() > 0) {
    226                     long newScreenId = LauncherAppState.getLauncherProvider().generateNewScreenId();
    227                     allScreens.add(newScreenId);
    228                     for (DbEntry item : placement.finalPlacedItems) {
    229                         if (!mCarryOver.remove(itemMap.get(item.id))) {
    230                             throw new Exception("Unable to find matching items");
    231                         }
    232                         item.screenId = newScreenId;
    233                         update(item);
    234                     }
    235                 } else {
    236                     throw new Exception("None of the items can be placed on an empty screen");
    237                 }
    238 
    239             } while (!mCarryOver.isEmpty());
    240 
    241             // Update screens
    242             final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
    243             mUpdateOperations.add(ContentProviderOperation.newDelete(uri).build());
    244             int count = allScreens.size();
    245             for (int i = 0; i < count; i++) {
    246                 ContentValues v = new ContentValues();
    247                 long screenId = allScreens.get(i);
    248                 v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
    249                 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
    250                 mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(v).build());
    251             }
    252         }
    253         return applyOperations();
    254     }
    255 
    256     /**
    257      * Migrate a particular screen id.
    258      * Strategy:
    259      *   1) For all possible combinations of row and column, pick the one which causes the least
    260      *      data loss: {@link #tryRemove(int, int, ArrayList, float[])}
    261      *   2) Maintain a list of all lost items before this screen, and add any new item lost from
    262      *      this screen to that list as well.
    263      *   3) If all those items from the above list can be placed on this screen, place them
    264      *      (otherwise they are placed on a new screen).
    265      */
    266     private void migrateScreen(long screenId) {
    267         ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
    268 
    269         int removedCol = Integer.MAX_VALUE;
    270         int removedRow = Integer.MAX_VALUE;
    271 
    272         // removeWt represents the cost function for loss of items during migration, and moveWt
    273         // represents the cost function for repositioning the items. moveWt is only considered if
    274         // removeWt is same for two different configurations.
    275         // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least
    276         // cost.
    277         float removeWt = Float.MAX_VALUE;
    278         float moveWt = Float.MAX_VALUE;
    279         float[] outLoss = new float[2];
    280         ArrayList<DbEntry> finalItems = null;
    281 
    282         // Try removing all possible combinations
    283         for (int x = 0; x < mSrcX; x++) {
    284             for (int y = 0; y < mSrcY; y++) {
    285                 // Use a deep copy when trying out a particular combination as it can change
    286                 // the underlying object.
    287                 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss);
    288 
    289                 if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) {
    290                     removeWt = outLoss[0];
    291                     moveWt = outLoss[1];
    292                     removedCol = mShouldRemoveX ? x : removedCol;
    293                     removedRow = mShouldRemoveY ? y : removedRow;
    294                     finalItems = itemsOnScreen;
    295                 }
    296 
    297                 // No need to loop over all rows, if a row removal is not needed.
    298                 if (!mShouldRemoveY) {
    299                     break;
    300                 }
    301             }
    302 
    303             if (!mShouldRemoveX) {
    304                 break;
    305             }
    306         }
    307 
    308         if (DEBUG) {
    309             Log.d(TAG, String.format("Removing row %d, column %d on screen %d",
    310                     removedRow, removedCol, screenId));
    311         }
    312 
    313         LongArrayMap<DbEntry> itemMap = new LongArrayMap<>();
    314         for (DbEntry e : deepCopy(items)) {
    315             itemMap.put(e.id, e);
    316         }
    317 
    318         for (DbEntry item : finalItems) {
    319             DbEntry org = itemMap.get(item.id);
    320             itemMap.remove(item.id);
    321 
    322             // Check if update is required
    323             if (!item.columnsSame(org)) {
    324                 update(item);
    325             }
    326         }
    327 
    328         // The remaining items in {@link #itemMap} are those which didn't get placed.
    329         for (DbEntry item : itemMap) {
    330             mCarryOver.add(item);
    331         }
    332 
    333         if (!mCarryOver.isEmpty() && removeWt == 0) {
    334             // No new items were removed in this step. Try placing all the items on this screen.
    335             boolean[][] occupied = new boolean[mTrgX][mTrgY];
    336             for (DbEntry item : finalItems) {
    337                 markCells(occupied, item, true);
    338             }
    339 
    340             OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
    341                     deepCopy(mCarryOver), true);
    342             placement.find();
    343             if (placement.lowestWeightLoss == 0) {
    344                 // All items got placed
    345 
    346                 for (DbEntry item : placement.finalPlacedItems) {
    347                     item.screenId = screenId;
    348                     update(item);
    349                 }
    350 
    351                 mCarryOver.clear();
    352             }
    353         }
    354     }
    355 
    356     /**
    357      * Updates an item in the DB.
    358      */
    359     private void update(DbEntry item) {
    360         mTempValues.clear();
    361         item.addToContentValues(mTempValues);
    362         mUpdateOperations.add(ContentProviderOperation
    363                 .newUpdate(LauncherSettings.Favorites.getContentUri(item.id))
    364                 .withValues(mTempValues).build());
    365     }
    366 
    367     /**
    368      * Tries the remove the provided row and column.
    369      * @param items all the items on the screen under operation
    370      * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
    371      * with the overall item movement.
    372      */
    373     private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items,
    374             float[] outLoss) {
    375         boolean[][] occupied = new boolean[mTrgX][mTrgY];
    376 
    377         col = mShouldRemoveX ? col : Integer.MAX_VALUE;
    378         row = mShouldRemoveY ? row : Integer.MAX_VALUE;
    379 
    380         ArrayList<DbEntry> finalItems = new ArrayList<>();
    381         ArrayList<DbEntry> removedItems = new ArrayList<>();
    382 
    383         for (DbEntry item : items) {
    384             if ((item.cellX <= col && (item.spanX + item.cellX) > col)
    385                 || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
    386                 removedItems.add(item);
    387                 if (item.cellX >= col) item.cellX --;
    388                 if (item.cellY >= row) item.cellY --;
    389             } else {
    390                 if (item.cellX > col) item.cellX --;
    391                 if (item.cellY > row) item.cellY --;
    392                 finalItems.add(item);
    393                 markCells(occupied, item, true);
    394             }
    395         }
    396 
    397         OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems);
    398         placement.find();
    399         finalItems.addAll(placement.finalPlacedItems);
    400         outLoss[0] = placement.lowestWeightLoss;
    401         outLoss[1] = placement.lowestMoveCost;
    402         return finalItems;
    403     }
    404 
    405     private void markCells(boolean[][] occupied, DbEntry item, boolean val) {
    406         for (int i = item.cellX; i < (item.cellX + item.spanX); i++) {
    407             for (int j = item.cellY; j < (item.cellY + item.spanY); j++) {
    408                 occupied[i][j] = val;
    409             }
    410         }
    411     }
    412 
    413     private boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) {
    414         if (x + w > mTrgX) return false;
    415         if (y + h > mTrgY) return false;
    416 
    417         for (int i = 0; i < w; i++) {
    418             for (int j = 0; j < h; j++) {
    419                 if (occupied[i + x][j + y]) {
    420                     return false;
    421                 }
    422             }
    423         }
    424         return true;
    425     }
    426 
    427     private class OptimalPlacementSolution {
    428         private final ArrayList<DbEntry> itemsToPlace;
    429         private final boolean[][] occupied;
    430 
    431         // If set to true, item movement are not considered in move cost, leading to a more
    432         // linear placement.
    433         private final boolean ignoreMove;
    434 
    435         float lowestWeightLoss = Float.MAX_VALUE;
    436         float lowestMoveCost = Float.MAX_VALUE;
    437         ArrayList<DbEntry> finalPlacedItems;
    438 
    439         public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) {
    440             this(occupied, itemsToPlace, false);
    441         }
    442 
    443         public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace,
    444                 boolean ignoreMove) {
    445             this.occupied = occupied;
    446             this.itemsToPlace = itemsToPlace;
    447             this.ignoreMove = ignoreMove;
    448 
    449             // Sort the items such that larger widgets appear first followed by 1x1 items
    450             Collections.sort(this.itemsToPlace);
    451         }
    452 
    453         public void find() {
    454             find(0, 0, 0, new ArrayList<DbEntry>());
    455         }
    456 
    457         /**
    458          * Recursively finds a placement for the provided items.
    459          * @param index the position in {@link #itemsToPlace} to start looking at.
    460          * @param weightLoss total weight loss upto this point
    461          * @param moveCost total move cost upto this point
    462          * @param itemsPlaced all the items already placed upto this point
    463          */
    464         public void find(int index, float weightLoss, float moveCost,
    465                 ArrayList<DbEntry> itemsPlaced) {
    466             if ((weightLoss >= lowestWeightLoss) ||
    467                     ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) {
    468                 // Abort, as we already have a better solution.
    469                 return;
    470 
    471             } else if (index >= itemsToPlace.size()) {
    472                 // End loop.
    473                 lowestWeightLoss = weightLoss;
    474                 lowestMoveCost = moveCost;
    475 
    476                 // Keep a deep copy of current configuration as it can change during recursion.
    477                 finalPlacedItems = deepCopy(itemsPlaced);
    478                 return;
    479             }
    480 
    481             DbEntry me = itemsToPlace.get(index);
    482             int myX = me.cellX;
    483             int myY = me.cellY;
    484 
    485             // List of items to pass over if this item was placed.
    486             ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1);
    487             itemsIncludingMe.addAll(itemsPlaced);
    488             itemsIncludingMe.add(me);
    489 
    490             if (me.spanX > 1 || me.spanY > 1) {
    491                 // If the current item is a widget (and it greater than 1x1), try to place it at
    492                 // all possible positions. This is because a widget placed at one position can
    493                 // affect the placement of a different widget.
    494                 int myW = me.spanX;
    495                 int myH = me.spanY;
    496 
    497                 for (int y = 0; y < mTrgY; y++) {
    498                     for (int x = 0; x < mTrgX; x++) {
    499                         float newMoveCost = moveCost;
    500                         if (x != myX) {
    501                             me.cellX = x;
    502                             newMoveCost ++;
    503                         }
    504                         if (y != myY) {
    505                             me.cellY = y;
    506                             newMoveCost ++;
    507                         }
    508                         if (ignoreMove) {
    509                             newMoveCost = moveCost;
    510                         }
    511 
    512                         if (isVacant(occupied, x, y, myW, myH)) {
    513                             // place at this position and continue search.
    514                             markCells(occupied, me, true);
    515                             find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
    516                             markCells(occupied, me, false);
    517                         }
    518 
    519                         // Try resizing horizontally
    520                         if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) {
    521                             me.spanX --;
    522                             markCells(occupied, me, true);
    523                             // 1 extra move cost
    524                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
    525                             markCells(occupied, me, false);
    526                             me.spanX ++;
    527                         }
    528 
    529                         // Try resizing vertically
    530                         if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) {
    531                             me.spanY --;
    532                             markCells(occupied, me, true);
    533                             // 1 extra move cost
    534                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
    535                             markCells(occupied, me, false);
    536                             me.spanY ++;
    537                         }
    538 
    539                         // Try resizing horizontally & vertically
    540                         if (myH > me.minSpanY && myW > me.minSpanX &&
    541                                 isVacant(occupied, x, y, myW - 1, myH - 1)) {
    542                             me.spanX --;
    543                             me.spanY --;
    544                             markCells(occupied, me, true);
    545                             // 2 extra move cost
    546                             find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
    547                             markCells(occupied, me, false);
    548                             me.spanX ++;
    549                             me.spanY ++;
    550                         }
    551                         me.cellX = myX;
    552                         me.cellY = myY;
    553                     }
    554                 }
    555 
    556                 // Finally also try a solution when this item is not included. Trying it in the end
    557                 // causes it to get skipped in most cases due to higher weight loss, and prevents
    558                 // unnecessary deep copies of various configurations.
    559                 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
    560             } else {
    561                 // Since this is a 1x1 item and all the following items are also 1x1, just place
    562                 // it at 'the most appropriate position' and hope for the best.
    563                 // The most appropriate position: one with lease straight line distance
    564                 int newDistance = Integer.MAX_VALUE;
    565                 int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;
    566 
    567                 for (int y = 0; y < mTrgY; y++) {
    568                     for (int x = 0; x < mTrgX; x++) {
    569                         if (!occupied[x][y]) {
    570                             int dist = ignoreMove ? 0 :
    571                                 ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y));
    572                             if (dist < newDistance) {
    573                                 newX = x;
    574                                 newY = y;
    575                                 newDistance = dist;
    576                             }
    577                         }
    578                     }
    579                 }
    580 
    581                 if (newX < mTrgX && newY < mTrgY) {
    582                     float newMoveCost = moveCost;
    583                     if (newX != myX) {
    584                         me.cellX = newX;
    585                         newMoveCost ++;
    586                     }
    587                     if (newY != myY) {
    588                         me.cellY = newY;
    589                         newMoveCost ++;
    590                     }
    591                     if (ignoreMove) {
    592                         newMoveCost = moveCost;
    593                     }
    594                     markCells(occupied, me, true);
    595                     find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
    596                     markCells(occupied, me, false);
    597                     me.cellX = myX;
    598                     me.cellY = myY;
    599 
    600                     // Try to find a solution without this item, only if
    601                     //  1) there was at least one space, i.e., we were able to place this item
    602                     //  2) if the next item has the same weight (all items are already sorted), as
    603                     //     if it has lower weight, that solution will automatically get discarded.
    604                     //  3) ignoreMove false otherwise, move cost is ignored and the weight will
    605                     //      anyway be same.
    606                     if (index + 1 < itemsToPlace.size()
    607                             && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) {
    608                         find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
    609                     }
    610                 } else {
    611                     // No more space. Jump to the end.
    612                     for (int i = index + 1; i < itemsToPlace.size(); i++) {
    613                         weightLoss += itemsToPlace.get(i).weight;
    614                     }
    615                     find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced);
    616                 }
    617             }
    618         }
    619     }
    620 
    621     private ArrayList<DbEntry> loadHotseatEntries() {
    622         Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
    623                 new String[]{
    624                         Favorites._ID,                  // 0
    625                         Favorites.ITEM_TYPE,            // 1
    626                         Favorites.INTENT,               // 2
    627                         Favorites.SCREEN},              // 3
    628                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null);
    629 
    630         final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
    631         final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
    632         final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
    633         final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN);
    634 
    635         ArrayList<DbEntry> entries = new ArrayList<>();
    636         while (c.moveToNext()) {
    637             DbEntry entry = new DbEntry();
    638             entry.id = c.getLong(indexId);
    639             entry.itemType = c.getInt(indexItemType);
    640             entry.screenId = c.getLong(indexScreen);
    641 
    642             if (entry.screenId >= mSrcHotseatSize) {
    643                 mEntryToRemove.add(entry.id);
    644                 continue;
    645             }
    646 
    647             try {
    648                 // calculate weight
    649                 switch (entry.itemType) {
    650                     case Favorites.ITEM_TYPE_SHORTCUT:
    651                     case Favorites.ITEM_TYPE_APPLICATION: {
    652                         verifyIntent(c.getString(indexIntent));
    653                         entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT
    654                                 ? WT_SHORTCUT : WT_APPLICATION;
    655                         break;
    656                     }
    657                     case Favorites.ITEM_TYPE_FOLDER: {
    658                         int total = getFolderItemsCount(entry.id);
    659                         if (total == 0) {
    660                             throw new Exception("Folder is empty");
    661                         }
    662                         entry.weight = WT_FOLDER_FACTOR * total;
    663                         break;
    664                     }
    665                     default:
    666                         throw new Exception("Invalid item type");
    667                 }
    668             } catch (Exception e) {
    669                 if (DEBUG) {
    670                     Log.d(TAG, "Removing item " + entry.id, e);
    671                 }
    672                 mEntryToRemove.add(entry.id);
    673                 continue;
    674             }
    675             entries.add(entry);
    676         }
    677         c.close();
    678         return entries;
    679     }
    680 
    681 
    682     /**
    683      * Loads entries for a particular screen id.
    684      */
    685     private ArrayList<DbEntry> loadWorkspaceEntries(long screen) {
    686         Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
    687                 new String[]{
    688                         Favorites._ID,                  // 0
    689                         Favorites.ITEM_TYPE,            // 1
    690                         Favorites.CELLX,                // 2
    691                         Favorites.CELLY,                // 3
    692                         Favorites.SPANX,                // 4
    693                         Favorites.SPANY,                // 5
    694                         Favorites.INTENT,               // 6
    695                         Favorites.APPWIDGET_PROVIDER,   // 7
    696                         Favorites.APPWIDGET_ID},        // 8
    697                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP
    698                         + " AND " + Favorites.SCREEN + " = " + screen, null, null, null);
    699 
    700         final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
    701         final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
    702         final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX);
    703         final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY);
    704         final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX);
    705         final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY);
    706         final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
    707         final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
    708         final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
    709 
    710         ArrayList<DbEntry> entries = new ArrayList<>();
    711         while (c.moveToNext()) {
    712             DbEntry entry = new DbEntry();
    713             entry.id = c.getLong(indexId);
    714             entry.itemType = c.getInt(indexItemType);
    715             entry.cellX = c.getInt(indexCellX);
    716             entry.cellY = c.getInt(indexCellY);
    717             entry.spanX = c.getInt(indexSpanX);
    718             entry.spanY = c.getInt(indexSpanY);
    719             entry.screenId = screen;
    720 
    721             try {
    722                 // calculate weight
    723                 switch (entry.itemType) {
    724                     case Favorites.ITEM_TYPE_SHORTCUT:
    725                     case Favorites.ITEM_TYPE_APPLICATION: {
    726                         verifyIntent(c.getString(indexIntent));
    727                         entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT
    728                             ? WT_SHORTCUT : WT_APPLICATION;
    729                         break;
    730                     }
    731                     case Favorites.ITEM_TYPE_APPWIDGET: {
    732                         String provider = c.getString(indexAppWidgetProvider);
    733                         ComponentName cn = ComponentName.unflattenFromString(provider);
    734                         verifyPackage(cn.getPackageName());
    735                         entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
    736                                 * entry.spanX * entry.spanY);
    737 
    738                         int widgetId = c.getInt(indexAppWidgetId);
    739                         LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance(
    740                                 mContext).getLauncherAppWidgetInfo(widgetId);
    741                         Point spans = pInfo == null ?
    742                                 mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext);
    743                         if (spans != null) {
    744                             entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
    745                             entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
    746                         } else {
    747                             // Assume that the widget be resized down to 2x2
    748                             entry.minSpanX = entry.minSpanY = 2;
    749                         }
    750 
    751                         if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
    752                             throw new Exception("Widget can't be resized down to fit the grid");
    753                         }
    754                         break;
    755                     }
    756                     case Favorites.ITEM_TYPE_FOLDER: {
    757                         int total = getFolderItemsCount(entry.id);
    758                         if (total == 0) {
    759                             throw new Exception("Folder is empty");
    760                         }
    761                         entry.weight = WT_FOLDER_FACTOR * total;
    762                         break;
    763                     }
    764                     default:
    765                         throw new Exception("Invalid item type");
    766                 }
    767             } catch (Exception e) {
    768                 if (DEBUG) {
    769                     Log.d(TAG, "Removing item " + entry.id, e);
    770                 }
    771                 mEntryToRemove.add(entry.id);
    772                 continue;
    773             }
    774             entries.add(entry);
    775         }
    776         c.close();
    777         return entries;
    778     }
    779 
    780     /**
    781      * @return the number of valid items in the folder.
    782      */
    783     private int getFolderItemsCount(long folderId) {
    784         Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
    785                 new String[]{Favorites._ID, Favorites.INTENT},
    786                 Favorites.CONTAINER + " = " + folderId, null, null, null);
    787 
    788         int total = 0;
    789         while (c.moveToNext()) {
    790             try {
    791                 verifyIntent(c.getString(1));
    792                 total++;
    793             } catch (Exception e) {
    794                 mEntryToRemove.add(c.getLong(0));
    795             }
    796         }
    797         c.close();
    798         return total;
    799     }
    800 
    801     /**
    802      * Verifies if the intent should be restored.
    803      */
    804     private void verifyIntent(String intentStr) throws Exception {
    805         Intent intent = Intent.parseUri(intentStr, 0);
    806         if (intent.getComponent() != null) {
    807             verifyPackage(intent.getComponent().getPackageName());
    808         } else if (intent.getPackage() != null) {
    809             // Only verify package if the component was null.
    810             verifyPackage(intent.getPackage());
    811         }
    812     }
    813 
    814     /**
    815      * Verifies if the package should be restored
    816      */
    817     private void verifyPackage(String packageName) throws Exception {
    818         if (!mValidPackages.contains(packageName)) {
    819             throw new Exception("Package not available");
    820         }
    821     }
    822 
    823     private static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
    824 
    825         public float weight;
    826 
    827         public DbEntry() { }
    828 
    829         public DbEntry copy() {
    830             DbEntry entry = new DbEntry();
    831             entry.copyFrom(this);
    832             entry.weight = weight;
    833             entry.minSpanX = minSpanX;
    834             entry.minSpanY = minSpanY;
    835             return entry;
    836         }
    837 
    838         /**
    839          * Comparator such that larger widgets come first,  followed by all 1x1 items
    840          * based on their weights.
    841          */
    842         @Override
    843         public int compareTo(DbEntry another) {
    844             if (itemType == Favorites.ITEM_TYPE_APPWIDGET) {
    845                 if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
    846                     return another.spanY * another.spanX - spanX * spanY;
    847                 } else {
    848                     return -1;
    849                 }
    850             } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
    851                 return 1;
    852             } else {
    853                 // Place higher weight before lower weight.
    854                 return Float.compare(another.weight, weight);
    855             }
    856         }
    857 
    858         public boolean columnsSame(DbEntry org) {
    859             return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX &&
    860                     org.spanY == spanY && org.screenId == screenId;
    861         }
    862 
    863         public void addToContentValues(ContentValues values) {
    864             values.put(LauncherSettings.Favorites.SCREEN, screenId);
    865             values.put(LauncherSettings.Favorites.CELLX, cellX);
    866             values.put(LauncherSettings.Favorites.CELLY, cellY);
    867             values.put(LauncherSettings.Favorites.SPANX, spanX);
    868             values.put(LauncherSettings.Favorites.SPANY, spanY);
    869         }
    870     }
    871 
    872     private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
    873         ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size());
    874         for (DbEntry e : src) {
    875             dup.add(e.copy());
    876         }
    877         return dup;
    878     }
    879 
    880     private static Point parsePoint(String point) {
    881         String[] split = point.split(",");
    882         return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
    883     }
    884 
    885     private static String getPointString(int x, int y) {
    886         return String.format(Locale.ENGLISH, "%d,%d", x, y);
    887     }
    888 
    889     public static void markForMigration(
    890             Context context, HashSet<String> widgets, BackupProtos.DeviceProfieData srcProfile) {
    891         Utilities.getPrefs(context).edit()
    892                 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE,
    893                         getPointString((int) srcProfile.desktopCols, (int) srcProfile.desktopRows))
    894                 .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE,
    895                         getPointString((int) srcProfile.hotseatCount, srcProfile.allappsRank))
    896                 .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets)
    897                 .apply();
    898     }
    899 
    900     /**
    901      * Migrates the workspace and hotseat in case their sizes changed.
    902      * @return false if the migration failed.
    903      */
    904     public static boolean migrateGridIfNeeded(Context context) {
    905         SharedPreferences prefs = Utilities.getPrefs(context);
    906         InvariantDeviceProfile idp = LauncherAppState.getInstance().getInvariantDeviceProfile();
    907 
    908         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
    909         String hotseatSizeString = getPointString(idp.numHotseatIcons, idp.hotseatAllAppsRank);
    910 
    911         if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
    912                 hotseatSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, ""))) {
    913             // Skip if workspace and hotseat sizes have not changed.
    914             return true;
    915         }
    916 
    917         long migrationStartTime = System.currentTimeMillis();
    918         try {
    919             boolean dbChanged = false;
    920 
    921             // Initialize list of valid packages. This contain all the packages which are already on
    922             // the device and packages which are being installed. Any item which doesn't belong to
    923             // this set is removed.
    924             // Since the loader removes such items anyway, removing these items here doesn't cause
    925             // any extra data loss and gives us more free space on the grid for better migration.
    926             HashSet validPackages = new HashSet<>();
    927             for (PackageInfo info : context.getPackageManager().getInstalledPackages(0)) {
    928                 validPackages.add(info.packageName);
    929             }
    930             validPackages.addAll(PackageInstallerCompat.getInstance(context)
    931                     .updateAndGetActiveSessionCache().keySet());
    932 
    933             // Hotseat
    934             Point srcHotseatSize = parsePoint(prefs.getString(
    935                     KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString));
    936             if (srcHotseatSize.x != idp.numHotseatIcons ||
    937                     srcHotseatSize.y != idp.hotseatAllAppsRank) {
    938                 // Migrate hotseat.
    939 
    940                 dbChanged = new GridSizeMigrationTask(context,
    941                         LauncherAppState.getInstance().getInvariantDeviceProfile(),
    942                         validPackages,
    943                         srcHotseatSize.x, srcHotseatSize.y,
    944                         idp.numHotseatIcons, idp.hotseatAllAppsRank).migrateHotseat();
    945             }
    946 
    947             // Grid size
    948             Point targetSize = new Point(idp.numColumns, idp.numRows);
    949             Point sourceSize = parsePoint(prefs.getString(
    950                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
    951 
    952             if (!targetSize.equals(sourceSize)) {
    953 
    954                 // The following list defines all possible grid sizes (and intermediate steps
    955                 // during migration). Note that at each step, dx <= 1 && dy <= 1. Any grid size
    956                 // which is not in this list is not migrated.
    957                 // Note that the InvariantDeviceProfile defines (rows, cols) but the Points
    958                 // specified here are defined as (cols, rows).
    959                 ArrayList<Point> gridSizeSteps = new ArrayList<>();
    960                 gridSizeSteps.add(new Point(3, 2));
    961                 gridSizeSteps.add(new Point(3, 3));
    962                 gridSizeSteps.add(new Point(4, 3));
    963                 gridSizeSteps.add(new Point(4, 4));
    964                 gridSizeSteps.add(new Point(5, 5));
    965                 gridSizeSteps.add(new Point(6, 5));
    966                 gridSizeSteps.add(new Point(6, 6));
    967                 gridSizeSteps.add(new Point(7, 7));
    968 
    969                 int sourceSizeIndex = gridSizeSteps.indexOf(sourceSize);
    970                 int targetSizeIndex = gridSizeSteps.indexOf(targetSize);
    971 
    972                 if (sourceSizeIndex <= -1 || targetSizeIndex <= -1) {
    973                     throw new Exception("Unable to migrate grid size from " + sourceSize
    974                             + " to " + targetSize);
    975                 }
    976 
    977                 // Min widget sizes
    978                 HashMap<String, Point> widgetMinSize = new HashMap<>();
    979                 for (String s : Utilities.getPrefs(context).getStringSet(KEY_MIGRATION_WIDGET_MINSIZE,
    980                         Collections.<String>emptySet())) {
    981                     String[] parts = s.split("#");
    982                     widgetMinSize.put(parts[0], parsePoint(parts[1]));
    983                 }
    984 
    985                 // Migrate the workspace grid, step by step.
    986                 while (targetSizeIndex < sourceSizeIndex ) {
    987                     // We only need to migrate the grid if source size is greater
    988                     // than the target size.
    989                     Point stepTargetSize = gridSizeSteps.get(sourceSizeIndex - 1);
    990                     Point stepSourceSize = gridSizeSteps.get(sourceSizeIndex);
    991 
    992                     if (new GridSizeMigrationTask(context,
    993                             LauncherAppState.getInstance().getInvariantDeviceProfile(),
    994                             validPackages, widgetMinSize,
    995                             stepSourceSize, stepTargetSize).migrateWorkspace()) {
    996                         dbChanged = true;
    997                     }
    998                     sourceSizeIndex--;
    999                 }
   1000             }
   1001 
   1002             if (dbChanged) {
   1003                 // Make sure we haven't removed everything.
   1004                 final Cursor c = context.getContentResolver().query(
   1005                         LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
   1006                 boolean hasData = c.moveToNext();
   1007                 c.close();
   1008                 if (!hasData) {
   1009                     throw new Exception("Removed every thing during grid resize");
   1010                 }
   1011             }
   1012 
   1013             return true;
   1014         } catch (Exception e) {
   1015             Log.e(TAG, "Error during grid migration", e);
   1016 
   1017             return false;
   1018         } finally {
   1019             Log.v(TAG, "Workspace migration completed in "
   1020                     + (System.currentTimeMillis() - migrationStartTime));
   1021 
   1022             // Save current configuration, so that the migration does not run again.
   1023             prefs.edit()
   1024                     .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
   1025                     .putString(KEY_MIGRATION_SRC_HOTSEAT_SIZE, hotseatSizeString)
   1026                     .remove(KEY_MIGRATION_WIDGET_MINSIZE)
   1027                     .apply();
   1028         }
   1029     }
   1030 }
   1031