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