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