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