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