• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3.model;
2 
3 import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
4 import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
5 import static com.android.launcher3.LauncherSettings.Settings.EXTRA_VALUE;
6 import static com.android.launcher3.Utilities.getPointString;
7 import static com.android.launcher3.Utilities.parsePoint;
8 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
9 
10 import android.content.ComponentName;
11 import android.content.ContentValues;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.content.SharedPreferences;
15 import android.content.pm.PackageInfo;
16 import android.content.pm.PackageManager;
17 import android.database.Cursor;
18 import android.database.sqlite.SQLiteDatabase;
19 import android.graphics.Point;
20 import android.os.SystemClock;
21 import android.util.Log;
22 import android.util.SparseArray;
23 
24 import androidx.annotation.VisibleForTesting;
25 
26 import com.android.launcher3.InvariantDeviceProfile;
27 import com.android.launcher3.LauncherAppState;
28 import com.android.launcher3.LauncherSettings;
29 import com.android.launcher3.LauncherSettings.Favorites;
30 import com.android.launcher3.LauncherSettings.Settings;
31 import com.android.launcher3.Utilities;
32 import com.android.launcher3.Workspace;
33 import com.android.launcher3.config.FeatureFlags;
34 import com.android.launcher3.graphics.LauncherPreviewRenderer;
35 import com.android.launcher3.model.data.ItemInfo;
36 import com.android.launcher3.pm.InstallSessionHelper;
37 import com.android.launcher3.provider.LauncherDbUtils;
38 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
39 import com.android.launcher3.util.GridOccupancy;
40 import com.android.launcher3.util.IntArray;
41 import com.android.launcher3.util.IntSparseArrayMap;
42 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
43 import com.android.launcher3.widget.WidgetManagerHelper;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.HashSet;
48 
49 /**
50  * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
51  * result of restoring from a larger device or device density change.
52  */
53 public class GridSizeMigrationTask {
54 
55     private static final String TAG = "GridSizeMigrationTask";
56     private static final boolean DEBUG = false;
57 
58     // These are carefully selected weights for various item types (Math.random?), to allow for
59     // the least absurd migration experience.
60     private static final float WT_SHORTCUT = 1;
61     private static final float WT_APPLICATION = 0.8f;
62     private static final float WT_WIDGET_MIN = 2;
63     private static final float WT_WIDGET_FACTOR = 0.6f;
64     private static final float WT_FOLDER_FACTOR = 0.5f;
65 
66     protected final SQLiteDatabase mDb;
67     protected final Context mContext;
68 
69     protected final IntArray mEntryToRemove = new IntArray();
70     protected final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
71 
72     private final SparseArray<ContentValues> mUpdateOperations = new SparseArray<>();
73     private final HashSet<String> mValidPackages;
74     private final String mTableName;
75 
76     private final int mSrcX, mSrcY;
77     private final int mTrgX, mTrgY;
78     private final boolean mShouldRemoveX, mShouldRemoveY;
79 
80     private final int mSrcHotseatSize;
81     private final int mDestHotseatSize;
82 
GridSizeMigrationTask(Context context, SQLiteDatabase db, HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize, Point targetSize)83     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
84             HashSet<String> validPackages, boolean usePreviewTable, Point sourceSize,
85             Point targetSize) {
86         mContext = context;
87         mDb = db;
88         mValidPackages = validPackages;
89         mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
90 
91         mSrcX = sourceSize.x;
92         mSrcY = sourceSize.y;
93 
94         mTrgX = targetSize.x;
95         mTrgY = targetSize.y;
96 
97         mShouldRemoveX = mTrgX < mSrcX;
98         mShouldRemoveY = mTrgY < mSrcY;
99 
100         // Non-used variables
101         mSrcHotseatSize = mDestHotseatSize = -1;
102     }
103 
104     protected GridSizeMigrationTask(Context context, SQLiteDatabase db,
105             HashSet<String> validPackages, boolean usePreviewTable, int srcHotseatSize,
106             int destHotseatSize) {
107         mContext = context;
108         mDb = db;
109         mValidPackages = validPackages;
110         mTableName = usePreviewTable ? Favorites.PREVIEW_TABLE_NAME : Favorites.TABLE_NAME;
111 
112         mSrcHotseatSize = srcHotseatSize;
113 
114         mDestHotseatSize = destHotseatSize;
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      *
124      * @return true if any DB operation was commited.
125      */
126     private boolean applyOperations() throws Exception {
127         // Update items
128         int updateCount = mUpdateOperations.size();
129         for (int i = 0; i < updateCount; i++) {
130             mDb.update(mTableName, mUpdateOperations.valueAt(i),
131                     "_id=" + mUpdateOperations.keyAt(i), null);
132         }
133 
134         if (!mEntryToRemove.isEmpty()) {
135             if (DEBUG) {
136                 Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
137             }
138             mDb.delete(mTableName, Utilities.createDbSelectionQuery(Favorites._ID, mEntryToRemove),
139                     null);
140         }
141 
142         return updateCount > 0 || !mEntryToRemove.isEmpty();
143     }
144 
145     /**
146      * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them
147      * in the order in the new hotseat while keeping an empty space for all-apps. If the number of
148      * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
149      * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
150      * & {@see #WT_FOLDER_FACTOR}.
151      *
152      * @return true if any DB change was made
153      */
154     protected boolean migrateHotseat() throws Exception {
155         ArrayList<DbEntry> items = loadHotseatEntries();
156         while (items.size() > mDestHotseatSize) {
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         }
187 
188         return applyOperations();
189     }
190 
191     @VisibleForTesting
192     static IntArray getWorkspaceScreenIds(SQLiteDatabase db, String tableName) {
193         return LauncherDbUtils.queryIntArray(db, tableName, Favorites.SCREEN,
194                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP,
195                 Favorites.SCREEN, Favorites.SCREEN);
196     }
197 
198     /**
199      * @return true if any DB change was made
200      */
201     protected boolean migrateWorkspace() throws Exception {
202         IntArray allScreens = getWorkspaceScreenIds(mDb, mTableName);
203         if (allScreens.isEmpty()) {
204             throw new Exception("Unable to get workspace screens");
205         }
206 
207         for (int i = 0; i < allScreens.size(); i++) {
208             int screenId = allScreens.get(i);
209             if (DEBUG) {
210                 Log.d(TAG, "Migrating " + screenId);
211             }
212             migrateScreen(screenId);
213         }
214 
215         if (!mCarryOver.isEmpty()) {
216             IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
217             for (DbEntry e : mCarryOver) {
218                 itemMap.put(e.id, e);
219             }
220 
221             do {
222                 // Some items are still remaining. Try adding a few new screens.
223 
224                 // At every iteration, make sure that at least one item is removed from
225                 // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
226                 // break the loop and abort migration by throwing an exception.
227                 OptimalPlacementSolution placement = new OptimalPlacementSolution(
228                         new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true);
229                 placement.find();
230                 if (placement.finalPlacedItems.size() > 0) {
231                     int newScreenId = LauncherSettings.Settings.call(
232                             mContext.getContentResolver(),
233                             LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
234                             .getInt(EXTRA_VALUE);
235                     for (DbEntry item : placement.finalPlacedItems) {
236                         if (!mCarryOver.remove(itemMap.get(item.id))) {
237                             throw new Exception("Unable to find matching items");
238                         }
239                         item.screenId = newScreenId;
240                         update(item);
241                     }
242                 } else {
243                     throw new Exception("None of the items can be placed on an empty screen");
244                 }
245 
246             } while (!mCarryOver.isEmpty());
247         }
248         return applyOperations();
249     }
250 
251     /**
252      * Migrate a particular screen id.
253      * Strategy:
254      *  1) For all possible combinations of row and column, pick the one which causes the least
255      *    data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
256      *  2) Maintain a list of all lost items before this screen, and add any new item lost from
257      *    this screen to that list as well.
258      *  3) If all those items from the above list can be placed on this screen, place them
259      *    (otherwise they are placed on a new screen).
260      */
261     protected void migrateScreen(int screenId) {
262         // If we are migrating the first screen, do not touch the first row.
263         int startY = (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID)
264                 ? 1 : 0;
265 
266         ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);
267 
268         int removedCol = Integer.MAX_VALUE;
269         int removedRow = Integer.MAX_VALUE;
270 
271         // removeWt represents the cost function for loss of items during migration, and moveWt
272         // represents the cost function for repositioning the items. moveWt is only considered if
273         // removeWt is same for two different configurations.
274         // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least
275         // cost.
276         float removeWt = Float.MAX_VALUE;
277         float moveWt = Float.MAX_VALUE;
278         float[] outLoss = new float[2];
279         ArrayList<DbEntry> finalItems = null;
280 
281         // Try removing all possible combinations
282         for (int x = 0; x < mSrcX; x++) {
283             // Try removing the rows first from bottom. This keeps the workspace
284             // nicely aligned with hotseat.
285             for (int y = mSrcY - 1; y >= startY; y--) {
286                 // Use a deep copy when trying out a particular combination as it can change
287                 // the underlying object.
288                 ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items),
289                         outLoss);
290 
291                 if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1]
292                         < moveWt))) {
293                     removeWt = outLoss[0];
294                     moveWt = outLoss[1];
295                     removedCol = mShouldRemoveX ? x : removedCol;
296                     removedRow = mShouldRemoveY ? y : removedRow;
297                     finalItems = itemsOnScreen;
298                 }
299 
300                 // No need to loop over all rows, if a row removal is not needed.
301                 if (!mShouldRemoveY) {
302                     break;
303                 }
304             }
305 
306             if (!mShouldRemoveX) {
307                 break;
308             }
309         }
310 
311         if (DEBUG) {
312             Log.d(TAG, String.format("Removing row %d, column %d on screen %d",
313                     removedRow, removedCol, screenId));
314         }
315 
316         IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
317         for (DbEntry e : deepCopy(items)) {
318             itemMap.put(e.id, e);
319         }
320 
321         for (DbEntry item : finalItems) {
322             DbEntry org = itemMap.get(item.id);
323             itemMap.remove(item.id);
324 
325             // Check if update is required
326             if (!item.columnsSame(org)) {
327                 update(item);
328             }
329         }
330 
331         // The remaining items in {@link #itemMap} are those which didn't get placed.
332         for (DbEntry item : itemMap) {
333             mCarryOver.add(item);
334         }
335 
336         if (!mCarryOver.isEmpty() && removeWt == 0) {
337             // No new items were removed in this step. Try placing all the items on this screen.
338             GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
339             occupied.markCells(0, 0, mTrgX, startY, true);
340             for (DbEntry item : finalItems) {
341                 occupied.markCells(item, true);
342             }
343 
344             OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
345                     deepCopy(mCarryOver), startY, true);
346             placement.find();
347             if (placement.lowestWeightLoss == 0) {
348                 // All items got placed
349 
350                 for (DbEntry item : placement.finalPlacedItems) {
351                     item.screenId = screenId;
352                     update(item);
353                 }
354 
355                 mCarryOver.clear();
356             }
357         }
358     }
359 
360     /**
361      * Updates an item in the DB.
362      */
363     protected void update(DbEntry item) {
364         ContentValues values = new ContentValues();
365         item.addToContentValues(values);
366         mUpdateOperations.put(item.id, values);
367     }
368 
369     /**
370      * Tries the remove the provided row and column.
371      *
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          *
448          * @param index       the position in {@link #itemsToPlace} to start looking at.
449          * @param weightLoss  total weight loss upto this point
450          * @param moveCost    total move cost upto this point
451          * @param itemsPlaced all the items already placed upto this point
452          */
453         public void find(int index, float weightLoss, float moveCost,
454                 ArrayList<DbEntry> itemsPlaced) {
455             if ((weightLoss >= lowestWeightLoss) ||
456                     ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) {
457                 // Abort, as we already have a better solution.
458                 return;
459 
460             } else if (index >= itemsToPlace.size()) {
461                 // End loop.
462                 lowestWeightLoss = weightLoss;
463                 lowestMoveCost = moveCost;
464 
465                 // Keep a deep copy of current configuration as it can change during recursion.
466                 finalPlacedItems = deepCopy(itemsPlaced);
467                 return;
468             }
469 
470             DbEntry me = itemsToPlace.get(index);
471             int myX = me.cellX;
472             int myY = me.cellY;
473 
474             // List of items to pass over if this item was placed.
475             ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1);
476             itemsIncludingMe.addAll(itemsPlaced);
477             itemsIncludingMe.add(me);
478 
479             if (me.spanX > 1 || me.spanY > 1) {
480                 // If the current item is a widget (and it greater than 1x1), try to place it at
481                 // all possible positions. This is because a widget placed at one position can
482                 // affect the placement of a different widget.
483                 int myW = me.spanX;
484                 int myH = me.spanY;
485 
486                 for (int y = startY; y < mTrgY; y++) {
487                     for (int x = 0; x < mTrgX; x++) {
488                         float newMoveCost = moveCost;
489                         if (x != myX) {
490                             me.cellX = x;
491                             newMoveCost++;
492                         }
493                         if (y != myY) {
494                             me.cellY = y;
495                             newMoveCost++;
496                         }
497                         if (ignoreMove) {
498                             newMoveCost = moveCost;
499                         }
500 
501                         if (occupied.isRegionVacant(x, y, myW, myH)) {
502                             // place at this position and continue search.
503                             occupied.markCells(me, true);
504                             find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
505                             occupied.markCells(me, false);
506                         }
507 
508                         // Try resizing horizontally
509                         if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
510                             me.spanX--;
511                             occupied.markCells(me, true);
512                             // 1 extra move cost
513                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
514                             occupied.markCells(me, false);
515                             me.spanX++;
516                         }
517 
518                         // Try resizing vertically
519                         if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
520                             me.spanY--;
521                             occupied.markCells(me, true);
522                             // 1 extra move cost
523                             find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
524                             occupied.markCells(me, false);
525                             me.spanY++;
526                         }
527 
528                         // Try resizing horizontally & vertically
529                         if (myH > me.minSpanY && myW > me.minSpanX &&
530                                 occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
531                             me.spanX--;
532                             me.spanY--;
533                             occupied.markCells(me, true);
534                             // 2 extra move cost
535                             find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
536                             occupied.markCells(me, false);
537                             me.spanX++;
538                             me.spanY++;
539                         }
540                         me.cellX = myX;
541                         me.cellY = myY;
542                     }
543                 }
544 
545                 // Finally also try a solution when this item is not included. Trying it in the end
546                 // causes it to get skipped in most cases due to higher weight loss, and prevents
547                 // unnecessary deep copies of various configurations.
548                 find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
549             } else {
550                 // Since this is a 1x1 item and all the following items are also 1x1, just place
551                 // it at 'the most appropriate position' and hope for the best.
552                 // The most appropriate position: one with lease straight line distance
553                 int newDistance = Integer.MAX_VALUE;
554                 int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;
555 
556                 for (int y = startY; y < mTrgY; y++) {
557                     for (int x = 0; x < mTrgX; x++) {
558                         if (!occupied.cells[x][y]) {
559                             int dist = ignoreMove ? 0 :
560                                     ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY
561                                             - y));
562                             if (dist < newDistance) {
563                                 newX = x;
564                                 newY = y;
565                                 newDistance = dist;
566                             }
567                         }
568                     }
569                 }
570 
571                 if (newX < mTrgX && newY < mTrgY) {
572                     float newMoveCost = moveCost;
573                     if (newX != myX) {
574                         me.cellX = newX;
575                         newMoveCost++;
576                     }
577                     if (newY != myY) {
578                         me.cellY = newY;
579                         newMoveCost++;
580                     }
581                     if (ignoreMove) {
582                         newMoveCost = moveCost;
583                     }
584                     occupied.markCells(me, true);
585                     find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
586                     occupied.markCells(me, false);
587                     me.cellX = myX;
588                     me.cellY = myY;
589 
590                     // Try to find a solution without this item, only if
591                     //  1) there was at least one space, i.e., we were able to place this item
592                     //  2) if the next item has the same weight (all items are already sorted), as
593                     //     if it has lower weight, that solution will automatically get discarded.
594                     //  3) ignoreMove false otherwise, move cost is ignored and the weight will
595                     //      anyway be same.
596                     if (index + 1 < itemsToPlace.size()
597                             && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) {
598                         find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
599                     }
600                 } else {
601                     // No more space. Jump to the end.
602                     for (int i = index + 1; i < itemsToPlace.size(); i++) {
603                         weightLoss += itemsToPlace.get(i).weight;
604                     }
605                     find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced);
606                 }
607             }
608         }
609     }
610 
611     private ArrayList<DbEntry> loadHotseatEntries() {
612         Cursor c = queryWorkspace(
613                 new String[]{
614                         Favorites._ID,                  // 0
615                         Favorites.ITEM_TYPE,            // 1
616                         Favorites.INTENT,               // 2
617                         Favorites.SCREEN},              // 3
618                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT);
619 
620         final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
621         final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
622         final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
623         final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN);
624 
625         ArrayList<DbEntry> entries = new ArrayList<>();
626         while (c.moveToNext()) {
627             DbEntry entry = new DbEntry();
628             entry.id = c.getInt(indexId);
629             entry.itemType = c.getInt(indexItemType);
630             entry.screenId = c.getInt(indexScreen);
631 
632             if (entry.screenId >= mSrcHotseatSize) {
633                 mEntryToRemove.add(entry.id);
634                 continue;
635             }
636 
637             try {
638                 // calculate weight
639                 switch (entry.itemType) {
640                     case Favorites.ITEM_TYPE_SHORTCUT:
641                     case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
642                     case Favorites.ITEM_TYPE_APPLICATION: {
643                         verifyIntent(c.getString(indexIntent));
644                         entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
645                                 WT_APPLICATION : WT_SHORTCUT;
646                         break;
647                     }
648                     case Favorites.ITEM_TYPE_FOLDER: {
649                         int total = getFolderItemsCount(entry.id);
650                         if (total == 0) {
651                             throw new Exception("Folder is empty");
652                         }
653                         entry.weight = WT_FOLDER_FACTOR * total;
654                         break;
655                     }
656                     default:
657                         throw new Exception("Invalid item type");
658                 }
659             } catch (Exception e) {
660                 if (DEBUG) {
661                     Log.d(TAG, "Removing item " + entry.id, e);
662                 }
663                 mEntryToRemove.add(entry.id);
664                 continue;
665             }
666             entries.add(entry);
667         }
668         c.close();
669         return entries;
670     }
671 
672 
673     /**
674      * Loads entries for a particular screen id.
675      */
676     protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
677         Cursor c = queryWorkspace(
678                 new String[]{
679                         Favorites._ID,                  // 0
680                         Favorites.ITEM_TYPE,            // 1
681                         Favorites.CELLX,                // 2
682                         Favorites.CELLY,                // 3
683                         Favorites.SPANX,                // 4
684                         Favorites.SPANY,                // 5
685                         Favorites.INTENT,               // 6
686                         Favorites.APPWIDGET_PROVIDER,   // 7
687                         Favorites.APPWIDGET_ID},        // 8
688                 Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP
689                         + " AND " + Favorites.SCREEN + " = " + screen);
690 
691         final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
692         final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
693         final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX);
694         final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY);
695         final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX);
696         final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY);
697         final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
698         final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
699         final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);
700 
701         ArrayList<DbEntry> entries = new ArrayList<>();
702         WidgetManagerHelper widgetManagerHelper = new WidgetManagerHelper(mContext);
703         while (c.moveToNext()) {
704             DbEntry entry = new DbEntry();
705             entry.id = c.getInt(indexId);
706             entry.itemType = c.getInt(indexItemType);
707             entry.cellX = c.getInt(indexCellX);
708             entry.cellY = c.getInt(indexCellY);
709             entry.spanX = c.getInt(indexSpanX);
710             entry.spanY = c.getInt(indexSpanY);
711             entry.screenId = screen;
712 
713             try {
714                 // calculate weight
715                 switch (entry.itemType) {
716                     case Favorites.ITEM_TYPE_SHORTCUT:
717                     case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
718                     case Favorites.ITEM_TYPE_APPLICATION: {
719                         verifyIntent(c.getString(indexIntent));
720                         entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
721                                 WT_APPLICATION : WT_SHORTCUT;
722                         break;
723                     }
724                     case Favorites.ITEM_TYPE_APPWIDGET: {
725                         String provider = c.getString(indexAppWidgetProvider);
726                         ComponentName cn = ComponentName.unflattenFromString(provider);
727                         verifyPackage(cn.getPackageName());
728                         entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
729                                 * entry.spanX * entry.spanY);
730 
731                         int widgetId = c.getInt(indexAppWidgetId);
732                         LauncherAppWidgetProviderInfo pInfo =
733                                 widgetManagerHelper.getLauncherAppWidgetInfo(widgetId);
734                         Point spans = null;
735                         if (pInfo != null) {
736                             spans = pInfo.getMinSpans();
737                         }
738                         if (spans != null) {
739                             entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
740                             entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
741                         } else {
742                             // Assume that the widget be resized down to 2x2
743                             entry.minSpanX = entry.minSpanY = 2;
744                         }
745 
746                         if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
747                             throw new Exception("Widget can't be resized down to fit the grid");
748                         }
749                         break;
750                     }
751                     case Favorites.ITEM_TYPE_FOLDER: {
752                         int total = getFolderItemsCount(entry.id);
753                         if (total == 0) {
754                             throw new Exception("Folder is empty");
755                         }
756                         entry.weight = WT_FOLDER_FACTOR * total;
757                         break;
758                     }
759                     default:
760                         throw new Exception("Invalid item type");
761                 }
762             } catch (Exception e) {
763                 if (DEBUG) {
764                     Log.d(TAG, "Removing item " + entry.id, e);
765                 }
766                 mEntryToRemove.add(entry.id);
767                 continue;
768             }
769             entries.add(entry);
770         }
771         c.close();
772         return entries;
773     }
774 
775     /**
776      * @return the number of valid items in the folder.
777      */
778     private int getFolderItemsCount(int folderId) {
779         Cursor c = queryWorkspace(
780                 new String[]{Favorites._ID, Favorites.INTENT},
781                 Favorites.CONTAINER + " = " + folderId);
782 
783         int total = 0;
784         while (c.moveToNext()) {
785             try {
786                 verifyIntent(c.getString(1));
787                 total++;
788             } catch (Exception e) {
789                 mEntryToRemove.add(c.getInt(0));
790             }
791         }
792         c.close();
793         return total;
794     }
795 
796     protected Cursor queryWorkspace(String[] columns, String where) {
797         return mDb.query(mTableName, columns, where, null, null, null, null);
798     }
799 
800     /**
801      * Verifies if the intent should be restored.
802      */
803     private void verifyIntent(String intentStr) throws Exception {
804         Intent intent = Intent.parseUri(intentStr, 0);
805         if (intent.getComponent() != null) {
806             verifyPackage(intent.getComponent().getPackageName());
807         } else if (intent.getPackage() != null) {
808             // Only verify package if the component was null.
809             verifyPackage(intent.getPackage());
810         }
811     }
812 
813     /**
814      * Verifies if the package should be restored
815      */
816     private void verifyPackage(String packageName) throws Exception {
817         if (!mValidPackages.contains(packageName)) {
818             throw new Exception("Package not available");
819         }
820     }
821 
822     protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
823 
824         public float weight;
825 
826         public DbEntry() {
827         }
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(Favorites.SCREEN, screenId);
865             values.put(Favorites.CELLX, cellX);
866             values.put(Favorites.CELLY, cellY);
867             values.put(Favorites.SPANX, spanX);
868             values.put(Favorites.SPANY, spanY);
869         }
870     }
871 
872     private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
873         ArrayList<DbEntry> dup = new ArrayList<>(src.size());
874         for (DbEntry e : src) {
875             dup.add(e.copy());
876         }
877         return dup;
878     }
879 
880     public static void markForMigration(
881             Context context, int gridX, int gridY, int hotseatSize) {
882         Utilities.getPrefs(context).edit()
883                 .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY))
884                 .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize)
885                 .apply();
886     }
887 
888     /**
889      * Check given a new IDP, if migration is necessary.
890      */
891     public static boolean needsToMigrate(Context context, InvariantDeviceProfile idp) {
892         SharedPreferences prefs = Utilities.getPrefs(context);
893         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
894 
895         return !gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, ""))
896                 || idp.numDatabaseHotseatIcons != prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1);
897     }
898 
899     /** See {@link #migrateGridIfNeeded(Context, InvariantDeviceProfile)} */
900     public static boolean migrateGridIfNeeded(Context context) {
901         if (context instanceof LauncherPreviewRenderer.PreviewContext) {
902             return true;
903         }
904         return migrateGridIfNeeded(context, null);
905     }
906 
907     /**
908      * Run the migration algorithm if needed. For preview, we provide the intended idp because it
909      * has not been changed. If idp is null, we read it from the context, for actual grid migration.
910      *
911      * @return false if the migration failed.
912      */
913     public static boolean migrateGridIfNeeded(Context context, InvariantDeviceProfile idp) {
914         boolean migrateForPreview = idp != null;
915         if (!migrateForPreview) {
916             idp = LauncherAppState.getIDP(context);
917         }
918 
919         if (!needsToMigrate(context, idp)) {
920             return true;
921         }
922 
923         SharedPreferences prefs = Utilities.getPrefs(context);
924         String gridSizeString = getPointString(idp.numColumns, idp.numRows);
925         long migrationStartTime = SystemClock.elapsedRealtime();
926         try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
927                 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
928                 .getBinder(Settings.EXTRA_VALUE)) {
929 
930             int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
931                     idp.numDatabaseHotseatIcons);
932             Point sourceSize = parsePoint(prefs.getString(
933                     KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));
934 
935             boolean dbChanged = false;
936             if (migrateForPreview) {
937                 copyTable(transaction.getDb(), Favorites.TABLE_NAME, transaction.getDb(),
938                         Favorites.PREVIEW_TABLE_NAME, context);
939             }
940 
941             GridBackupTable backupTable = new GridBackupTable(context, transaction.getDb(),
942                     srcHotseatCount, sourceSize.x, sourceSize.y);
943             if (migrateForPreview ? backupTable.restoreToPreviewIfBackupExists()
944                     : backupTable.backupOrRestoreAsNeeded()) {
945                 dbChanged = true;
946                 srcHotseatCount = backupTable.getRestoreHotseatAndGridSize(sourceSize);
947             }
948 
949             HashSet<String> validPackages = getValidPackages(context);
950             // Hotseat.
951             if (srcHotseatCount != idp.numDatabaseHotseatIcons
952                     && new GridSizeMigrationTask(context, transaction.getDb(), validPackages,
953                             migrateForPreview, srcHotseatCount,
954                             idp.numDatabaseHotseatIcons).migrateHotseat()) {
955                 dbChanged = true;
956             }
957 
958             // Grid size
959             Point targetSize = new Point(idp.numColumns, idp.numRows);
960             if (new MultiStepMigrationTask(validPackages, context, transaction.getDb(),
961                     migrateForPreview).migrate(sourceSize, targetSize)) {
962                 dbChanged = true;
963             }
964 
965             if (dbChanged) {
966                 // Make sure we haven't removed everything.
967                 final Cursor c = context.getContentResolver().query(
968                         migrateForPreview ? Favorites.PREVIEW_CONTENT_URI : Favorites.CONTENT_URI,
969                         null, null, null, null);
970                 boolean hasData = c.moveToNext();
971                 c.close();
972                 if (!hasData) {
973                     throw new Exception("Removed every thing during grid resize");
974                 }
975             }
976 
977             transaction.commit();
978             if (!migrateForPreview) {
979                 Settings.call(context.getContentResolver(), Settings.METHOD_REFRESH_BACKUP_TABLE);
980             }
981             return true;
982         } catch (Exception e) {
983             Log.e(TAG, "Error during preview grid migration", e);
984 
985             return false;
986         } finally {
987             Log.v(TAG, "Preview workspace migration completed in "
988                     + (SystemClock.elapsedRealtime() - migrationStartTime));
989 
990             if (!migrateForPreview) {
991                 // Save current configuration, so that the migration does not run again.
992                 prefs.edit()
993                         .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
994                         .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numDatabaseHotseatIcons)
995                         .apply();
996             }
997         }
998     }
999 
1000     protected static HashSet<String> getValidPackages(Context context) {
1001         // Initialize list of valid packages. This contain all the packages which are already on
1002         // the device and packages which are being installed. Any item which doesn't belong to
1003         // this set is removed.
1004         // Since the loader removes such items anyway, removing these items here doesn't cause
1005         // any extra data loss and gives us more free space on the grid for better migration.
1006         HashSet<String> validPackages = new HashSet<>();
1007         for (PackageInfo info : context.getPackageManager()
1008                 .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
1009             validPackages.add(info.packageName);
1010         }
1011         InstallSessionHelper.INSTANCE.get(context)
1012                 .getActiveSessions().keySet()
1013                 .forEach(packageUserKey -> validPackages.add(packageUserKey.mPackageName));
1014         return validPackages;
1015     }
1016 
1017     /**
1018      * Removes any broken item from the hotseat.
1019      *
1020      * @return a map with occupied hotseat position set to non-null value.
1021      */
1022     public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
1023             throws Exception {
1024         try (SQLiteTransaction transaction = (SQLiteTransaction) Settings.call(
1025                 context.getContentResolver(), Settings.METHOD_NEW_TRANSACTION)
1026                 .getBinder(Settings.EXTRA_VALUE)) {
1027             GridSizeMigrationTask task = new GridSizeMigrationTask(
1028                     context, transaction.getDb(), getValidPackages(context),
1029                     false /* usePreviewTable */, Integer.MAX_VALUE, Integer.MAX_VALUE);
1030 
1031             // Load all the valid entries
1032             ArrayList<DbEntry> items = task.loadHotseatEntries();
1033             // Delete any entry marked for deletion by above load.
1034             task.applyOperations();
1035             IntSparseArrayMap<Object> positions = new IntSparseArrayMap<>();
1036             for (DbEntry item : items) {
1037                 positions.put(item.screenId, item);
1038             }
1039             transaction.commit();
1040             return positions;
1041         }
1042     }
1043 
1044     /**
1045      * Task to run grid migration in multiple steps when the size difference is more than 1.
1046      */
1047     protected static class MultiStepMigrationTask {
1048         private final HashSet<String> mValidPackages;
1049         private final Context mContext;
1050         private final SQLiteDatabase mDb;
1051         private final boolean mUsePreviewTable;
1052 
1053         public MultiStepMigrationTask(HashSet<String> validPackages, Context context,
1054                 SQLiteDatabase db, boolean usePreviewTable) {
1055             mValidPackages = validPackages;
1056             mContext = context;
1057             mDb = db;
1058             mUsePreviewTable = usePreviewTable;
1059         }
1060 
1061         public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
1062             boolean dbChanged = false;
1063             if (!targetSize.equals(sourceSize)) {
1064                 if (sourceSize.x < targetSize.x) {
1065                     // Source is smaller that target, just expand the grid without actual migration.
1066                     sourceSize.x = targetSize.x;
1067                 }
1068                 if (sourceSize.y < targetSize.y) {
1069                     // Source is smaller that target, just expand the grid without actual migration.
1070                     sourceSize.y = targetSize.y;
1071                 }
1072 
1073                 // Migrate the workspace grid, such that the points differ by max 1 in x and y
1074                 // each on every step.
1075                 while (!targetSize.equals(sourceSize)) {
1076                     // Get the next size, such that the points differ by max 1 in x and y each
1077                     Point nextSize = new Point(sourceSize);
1078                     if (targetSize.x < nextSize.x) {
1079                         nextSize.x--;
1080                     }
1081                     if (targetSize.y < nextSize.y) {
1082                         nextSize.y--;
1083                     }
1084                     if (runStepTask(sourceSize, nextSize)) {
1085                         dbChanged = true;
1086                     }
1087                     sourceSize.set(nextSize.x, nextSize.y);
1088                 }
1089             }
1090             return dbChanged;
1091         }
1092 
1093         protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
1094             return new GridSizeMigrationTask(mContext, mDb, mValidPackages, mUsePreviewTable,
1095                     sourceSize, nextSize).migrateWorkspace();
1096         }
1097     }
1098 }
1099