• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
GridSizeMigrationTask(Context context, InvariantDeviceProfile idp, HashSet<String> validPackages, Point sourceSize, Point targetSize)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);
placement.find()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