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