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