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.database.Cursor;
11import android.graphics.Point;
12import android.text.TextUtils;
13import android.util.Log;
14
15import com.android.launcher3.InvariantDeviceProfile;
16import com.android.launcher3.ItemInfo;
17import com.android.launcher3.LauncherAppState;
18import com.android.launcher3.LauncherAppWidgetProviderInfo;
19import com.android.launcher3.LauncherModel;
20import com.android.launcher3.LauncherProvider;
21import com.android.launcher3.LauncherSettings;
22import com.android.launcher3.LauncherSettings.Favorites;
23import com.android.launcher3.Utilities;
24import com.android.launcher3.compat.PackageInstallerCompat;
25import com.android.launcher3.compat.UserHandleCompat;
26import com.android.launcher3.util.LongArrayMap;
27import com.android.launcher3.util.Thunk;
28
29import java.util.ArrayList;
30import java.util.Collections;
31import java.util.HashMap;
32import java.util.HashSet;
33
34/**
35 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
36 * result of restoring from a larger device.
37 */
38public class MigrateFromRestoreTask {
39
40    public static boolean ENABLED = false;
41
42    private static final String TAG = "MigrateFromRestoreTask";
43    private static final boolean DEBUG = true;
44
45    private static final String KEY_MIGRATION_SOURCE_SIZE = "migration_restore_src_size";
46    private static final String KEY_MIGRATION_WIDGET_MINSIZE = "migration_widget_min_size";
47
48    // These are carefully selected weights for various item types (Math.random?), to allow for
49    // the lease absurd migration experience.
50    private static final float WT_SHORTCUT = 1;
51    private static final float WT_APPLICATION = 0.8f;
52    private static final float WT_WIDGET_MIN = 2;
53    private static final float WT_WIDGET_FACTOR = 0.6f;
54    private static final float WT_FOLDER_FACTOR = 0.5f;
55
56    private final Context mContext;
57    private final ContentValues mTempValues = new ContentValues();
58    private final HashMap<String, Point> mWidgetMinSize;
59    private final InvariantDeviceProfile mIdp;
60
61    private HashSet<String> mValidPackages;
62    public ArrayList<Long> mEntryToRemove;
63    private ArrayList<ContentProviderOperation> mUpdateOperations;
64
65    private ArrayList<DbEntry> mCarryOver;
66
67    private final int mSrcX, mSrcY;
68    @Thunk final int mTrgX, mTrgY;
69    private final boolean mShouldRemoveX, mShouldRemoveY;
70
71    public MigrateFromRestoreTask(Context context) {
72        mContext = context;
73
74        SharedPreferences prefs = prefs(context);
75        Point sourceSize = parsePoint(prefs.getString(KEY_MIGRATION_SOURCE_SIZE, ""));
76        mSrcX = sourceSize.x;
77        mSrcY = sourceSize.y;
78
79        mWidgetMinSize = new HashMap<String, Point>();
80        for (String s : prefs.getStringSet(KEY_MIGRATION_WIDGET_MINSIZE,
81                Collections.<String>emptySet())) {
82            String[] parts = s.split("#");
83            mWidgetMinSize.put(parts[0], parsePoint(parts[1]));
84        }
85
86        mIdp = LauncherAppState.getInstance().getInvariantDeviceProfile();
87        mTrgX = mIdp.numColumns;
88        mTrgY = mIdp.numRows;
89        mShouldRemoveX = mTrgX < mSrcX;
90        mShouldRemoveY = mTrgY < mSrcY;
91    }
92
93    public void execute() throws Exception {
94        mEntryToRemove = new ArrayList<>();
95        mCarryOver = new ArrayList<>();
96        mUpdateOperations = new ArrayList<>();
97
98        // Initialize list of valid packages. This contain all the packages which are already on
99        // the device and packages which are being installed. Any item which doesn't belong to
100        // this set is removed.
101        // Since the loader removes such items anyway, removing these items here doesn't cause any
102        // extra data loss and gives us more free space on the grid for better migration.
103        mValidPackages = new HashSet<>();
104        for (PackageInfo info : mContext.getPackageManager().getInstalledPackages(0)) {
105            mValidPackages.add(info.packageName);
106        }
107        mValidPackages.addAll(PackageInstallerCompat.getInstance(mContext)
108                .updateAndGetActiveSessionCache().keySet());
109
110        ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(mContext);
111        if (allScreens.isEmpty()) {
112            throw new Exception("Unable to get workspace screens");
113        }
114
115        for (long screenId : allScreens) {
116            if (DEBUG) {
117                Log.d(TAG, "Migrating " + screenId);
118            }
119            migrateScreen(screenId);
120        }
121
122        if (!mCarryOver.isEmpty()) {
123            LongArrayMap<DbEntry> itemMap = new LongArrayMap<>();
124            for (DbEntry e : mCarryOver) {
125                itemMap.put(e.id, e);
126            }
127
128            do {
129                // Some items are still remaining. Try adding a few new screens.
130
131                // At every iteration, make sure that at least one item is removed from
132                // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
133                // break the loop and abort migration by throwing an exception.
134                OptimalPlacementSolution placement = new OptimalPlacementSolution(
135                        new boolean[mTrgX][mTrgY], deepCopy(mCarryOver), true);
136                placement.find();
137                if (placement.finalPlacedItems.size() > 0) {
138                    long newScreenId = LauncherAppState.getLauncherProvider().generateNewScreenId();
139                    allScreens.add(newScreenId);
140                    for (DbEntry item : placement.finalPlacedItems) {
141                        if (!mCarryOver.remove(itemMap.get(item.id))) {
142                            throw new Exception("Unable to find matching items");
143                        }
144                        item.screenId = newScreenId;
145                        update(item);
146                    }
147                } else {
148                    throw new Exception("None of the items can be placed on an empty screen");
149                }
150
151            } while (!mCarryOver.isEmpty());
152
153
154            LauncherAppState.getInstance().getModel()
155                .updateWorkspaceScreenOrder(mContext, allScreens);
156        }
157
158        // Update items
159        mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations);
160
161        if (!mEntryToRemove.isEmpty()) {
162            if (DEBUG) {
163                Log.d(TAG, "Removing items: " + TextUtils.join(", ", mEntryToRemove));
164            }
165            mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
166                    Utilities.createDbSelectionQuery(
167                            LauncherSettings.Favorites._ID, mEntryToRemove), null);
168        }
169
170        // Make sure we haven't removed everything.
171        final Cursor c = mContext.getContentResolver().query(
172                LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
173        boolean hasData = c.moveToNext();
174        c.close();
175        if (!hasData) {
176            throw new Exception("Removed every thing during grid resize");
177        }
178    }
179
180    /**
181     * Migrate a particular screen id.
182     * Strategy:
183     *   1) For all possible combinations of row and column, pick the one which causes the least
184     *      data loss: {@link #tryRemove(int, int, ArrayList, float[])}
185     *   2) Maintain a list of all lost items before this screen, and add any new item lost from
186     *      this screen to that list as well.
187     *   3) If all those items from the above list can be placed on this screen, place them
188     *      (otherwise they are placed on a new screen).
189     */
190    private void migrateScreen(long screenId) {
191        ArrayList<DbEntry> items = loadEntries(screenId);
192
193        int removedCol = Integer.MAX_VALUE;
194        int removedRow = Integer.MAX_VALUE;
195
196        // removeWt represents the cost function for loss of items during migration, and moveWt
197        // represents the cost function for repositioning the items. moveWt is only considered if
198        // removeWt is same for two different configurations.
199        // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least
200        // cost.
201        float removeWt = Float.MAX_VALUE;
202        float moveWt = Float.MAX_VALUE;
203        float[] outLoss = new float[2];
204        ArrayList<DbEntry> finalItems = null;
205
206        // Try removing all possible combinations
207        for (int x = 0; x < mSrcX; x++) {
208            for (int y = 0; y < mSrcY; y++) {
209                // Use a deep copy when trying out a particular combination as it can change
210                // the underlying object.
211                ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, deepCopy(items), outLoss);
212
213                if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1] < moveWt))) {
214                    removeWt = outLoss[0];
215                    moveWt = outLoss[1];
216                    removedCol = mShouldRemoveX ? x : removedCol;
217                    removedRow = mShouldRemoveY ? y : removedRow;
218                    finalItems = itemsOnScreen;
219                }
220
221                // No need to loop over all rows, if a row removal is not needed.
222                if (!mShouldRemoveY) {
223                    break;
224                }
225            }
226
227            if (!mShouldRemoveX) {
228                break;
229            }
230        }
231
232        if (DEBUG) {
233            Log.d(TAG, String.format("Removing row %d, column %d on screen %d",
234                    removedRow, removedCol, screenId));
235        }
236
237        LongArrayMap<DbEntry> itemMap = new LongArrayMap<>();
238        for (DbEntry e : deepCopy(items)) {
239            itemMap.put(e.id, e);
240        }
241
242        for (DbEntry item : finalItems) {
243            DbEntry org = itemMap.get(item.id);
244            itemMap.remove(item.id);
245
246            // Check if update is required
247            if (!item.columnsSame(org)) {
248                update(item);
249            }
250        }
251
252        // The remaining items in {@link #itemMap} are those which didn't get placed.
253        for (DbEntry item : itemMap) {
254            mCarryOver.add(item);
255        }
256
257        if (!mCarryOver.isEmpty() && removeWt == 0) {
258            // No new items were removed in this step. Try placing all the items on this screen.
259            boolean[][] occupied = new boolean[mTrgX][mTrgY];
260            for (DbEntry item : finalItems) {
261                markCells(occupied, item, true);
262            }
263
264            OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
265                    deepCopy(mCarryOver), true);
266            placement.find();
267            if (placement.lowestWeightLoss == 0) {
268                // All items got placed
269
270                for (DbEntry item : placement.finalPlacedItems) {
271                    item.screenId = screenId;
272                    update(item);
273                }
274
275                mCarryOver.clear();
276            }
277        }
278    }
279
280    /**
281     * Updates an item in the DB.
282     */
283    private void update(DbEntry item) {
284        mTempValues.clear();
285        item.addToContentValues(mTempValues);
286        mUpdateOperations.add(ContentProviderOperation
287                .newUpdate(LauncherSettings.Favorites.getContentUri(item.id))
288                .withValues(mTempValues).build());
289    }
290
291    /**
292     * Tries the remove the provided row and column.
293     * @param items all the items on the screen under operation
294     * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
295     * with the overall item movement.
296     */
297    private ArrayList<DbEntry> tryRemove(int col, int row, ArrayList<DbEntry> items,
298            float[] outLoss) {
299        boolean[][] occupied = new boolean[mTrgX][mTrgY];
300
301        col = mShouldRemoveX ? col : Integer.MAX_VALUE;
302        row = mShouldRemoveY ? row : Integer.MAX_VALUE;
303
304        ArrayList<DbEntry> finalItems = new ArrayList<>();
305        ArrayList<DbEntry> removedItems = new ArrayList<>();
306
307        for (DbEntry item : items) {
308            if ((item.cellX <= col && (item.spanX + item.cellX) > col)
309                || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
310                removedItems.add(item);
311                if (item.cellX >= col) item.cellX --;
312                if (item.cellY >= row) item.cellY --;
313            } else {
314                if (item.cellX > col) item.cellX --;
315                if (item.cellY > row) item.cellY --;
316                finalItems.add(item);
317                markCells(occupied, item, true);
318            }
319        }
320
321        OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied, removedItems);
322        placement.find();
323        finalItems.addAll(placement.finalPlacedItems);
324        outLoss[0] = placement.lowestWeightLoss;
325        outLoss[1] = placement.lowestMoveCost;
326        return finalItems;
327    }
328
329    @Thunk void markCells(boolean[][] occupied, DbEntry item, boolean val) {
330        for (int i = item.cellX; i < (item.cellX + item.spanX); i++) {
331            for (int j = item.cellY; j < (item.cellY + item.spanY); j++) {
332                occupied[i][j] = val;
333            }
334        }
335    }
336
337    @Thunk boolean isVacant(boolean[][] occupied, int x, int y, int w, int h) {
338        if (x + w > mTrgX) return false;
339        if (y + h > mTrgY) return false;
340
341        for (int i = 0; i < w; i++) {
342            for (int j = 0; j < h; j++) {
343                if (occupied[i + x][j + y]) {
344                    return false;
345                }
346            }
347        }
348        return true;
349    }
350
351    private class OptimalPlacementSolution {
352        private final ArrayList<DbEntry> itemsToPlace;
353        private final boolean[][] occupied;
354
355        // If set to true, item movement are not considered in move cost, leading to a more
356        // linear placement.
357        private final boolean ignoreMove;
358
359        float lowestWeightLoss = Float.MAX_VALUE;
360        float lowestMoveCost = Float.MAX_VALUE;
361        ArrayList<DbEntry> finalPlacedItems;
362
363        public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace) {
364            this(occupied, itemsToPlace, false);
365        }
366
367        public OptimalPlacementSolution(boolean[][] occupied, ArrayList<DbEntry> itemsToPlace,
368                boolean ignoreMove) {
369            this.occupied = occupied;
370            this.itemsToPlace = itemsToPlace;
371            this.ignoreMove = ignoreMove;
372
373            // Sort the items such that larger widgets appear first followed by 1x1 items
374            Collections.sort(this.itemsToPlace);
375        }
376
377        public void find() {
378            find(0, 0, 0, new ArrayList<DbEntry>());
379        }
380
381        /**
382         * Recursively finds a placement for the provided items.
383         * @param index the position in {@link #itemsToPlace} to start looking at.
384         * @param weightLoss total weight loss upto this point
385         * @param moveCost total move cost upto this point
386         * @param itemsPlaced all the items already placed upto this point
387         */
388        public void find(int index, float weightLoss, float moveCost,
389                ArrayList<DbEntry> itemsPlaced) {
390            if ((weightLoss >= lowestWeightLoss) ||
391                    ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) {
392                // Abort, as we already have a better solution.
393                return;
394
395            } else if (index >= itemsToPlace.size()) {
396                // End loop.
397                lowestWeightLoss = weightLoss;
398                lowestMoveCost = moveCost;
399
400                // Keep a deep copy of current configuration as it can change during recursion.
401                finalPlacedItems = deepCopy(itemsPlaced);
402                return;
403            }
404
405            DbEntry me = itemsToPlace.get(index);
406            int myX = me.cellX;
407            int myY = me.cellY;
408
409            // List of items to pass over if this item was placed.
410            ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1);
411            itemsIncludingMe.addAll(itemsPlaced);
412            itemsIncludingMe.add(me);
413
414            if (me.spanX > 1 || me.spanY > 1) {
415                // If the current item is a widget (and it greater than 1x1), try to place it at
416                // all possible positions. This is because a widget placed at one position can
417                // affect the placement of a different widget.
418                int myW = me.spanX;
419                int myH = me.spanY;
420
421                for (int y = 0; y < mTrgY; y++) {
422                    for (int x = 0; x < mTrgX; x++) {
423                        float newMoveCost = moveCost;
424                        if (x != myX) {
425                            me.cellX = x;
426                            newMoveCost ++;
427                        }
428                        if (y != myY) {
429                            me.cellY = y;
430                            newMoveCost ++;
431                        }
432                        if (ignoreMove) {
433                            newMoveCost = moveCost;
434                        }
435
436                        if (isVacant(occupied, x, y, myW, myH)) {
437                            // place at this position and continue search.
438                            markCells(occupied, me, true);
439                            find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
440                            markCells(occupied, me, false);
441                        }
442
443                        // Try resizing horizontally
444                        if (myW > me.minSpanX && isVacant(occupied, x, y, myW - 1, myH)) {
445                            me.spanX --;
446                            markCells(occupied, me, true);
447                            // 1 extra move cost
448                            find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
449                            markCells(occupied, me, false);
450                            me.spanX ++;
451                        }
452
453                        // Try resizing vertically
454                        if (myH > me.minSpanY && isVacant(occupied, x, y, myW, myH - 1)) {
455                            me.spanY --;
456                            markCells(occupied, me, true);
457                            // 1 extra move cost
458                            find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
459                            markCells(occupied, me, false);
460                            me.spanY ++;
461                        }
462
463                        // Try resizing horizontally & vertically
464                        if (myH > me.minSpanY && myW > me.minSpanX &&
465                                isVacant(occupied, x, y, myW - 1, myH - 1)) {
466                            me.spanX --;
467                            me.spanY --;
468                            markCells(occupied, me, true);
469                            // 2 extra move cost
470                            find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
471                            markCells(occupied, me, false);
472                            me.spanX ++;
473                            me.spanY ++;
474                        }
475                        me.cellX = myX;
476                        me.cellY = myY;
477                    }
478                }
479
480                // Finally also try a solution when this item is not included. Trying it in the end
481                // causes it to get skipped in most cases due to higher weight loss, and prevents
482                // unnecessary deep copies of various configurations.
483                find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
484            } else {
485                // Since this is a 1x1 item and all the following items are also 1x1, just place
486                // it at 'the most appropriate position' and hope for the best.
487                // The most appropriate position: one with lease straight line distance
488                int newDistance = Integer.MAX_VALUE;
489                int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;
490
491                for (int y = 0; y < mTrgY; y++) {
492                    for (int x = 0; x < mTrgX; x++) {
493                        if (!occupied[x][y]) {
494                            int dist = ignoreMove ? 0 :
495                                ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY - y));
496                            if (dist < newDistance) {
497                                newX = x;
498                                newY = y;
499                                newDistance = dist;
500                            }
501                        }
502                    }
503                }
504
505                if (newX < mTrgX && newY < mTrgY) {
506                    float newMoveCost = moveCost;
507                    if (newX != myX) {
508                        me.cellX = newX;
509                        newMoveCost ++;
510                    }
511                    if (newY != myY) {
512                        me.cellY = newY;
513                        newMoveCost ++;
514                    }
515                    if (ignoreMove) {
516                        newMoveCost = moveCost;
517                    }
518                    markCells(occupied, me, true);
519                    find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
520                    markCells(occupied, me, false);
521                    me.cellX = myX;
522                    me.cellY = myY;
523
524                    // Try to find a solution without this item, only if
525                    //  1) there was at least one space, i.e., we were able to place this item
526                    //  2) if the next item has the same weight (all items are already sorted), as
527                    //     if it has lower weight, that solution will automatically get discarded.
528                    //  3) ignoreMove false otherwise, move cost is ignored and the weight will
529                    //      anyway be same.
530                    if (index + 1 < itemsToPlace.size()
531                            && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) {
532                        find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
533                    }
534                } else {
535                    // No more space. Jump to the end.
536                    for (int i = index + 1; i < itemsToPlace.size(); i++) {
537                        weightLoss += itemsToPlace.get(i).weight;
538                    }
539                    find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced);
540                }
541            }
542        }
543    }
544
545    /**
546     * Loads entries for a particular screen id.
547     */
548    public ArrayList<DbEntry> loadEntries(long screen) {
549       Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
550                new String[] {
551                    Favorites._ID,                  // 0
552                    Favorites.ITEM_TYPE,            // 1
553                    Favorites.CELLX,                // 2
554                    Favorites.CELLY,                // 3
555                    Favorites.SPANX,                // 4
556                    Favorites.SPANY,                // 5
557                    Favorites.INTENT,               // 6
558                    Favorites.APPWIDGET_PROVIDER},  // 7
559                Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP
560                    + " AND " + Favorites.SCREEN + " = " + screen, null, null, null);
561
562       final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
563       final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
564       final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX);
565       final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY);
566       final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX);
567       final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY);
568       final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
569       final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
570
571       ArrayList<DbEntry> entries = new ArrayList<>();
572       while (c.moveToNext()) {
573           DbEntry entry = new DbEntry();
574           entry.id = c.getLong(indexId);
575           entry.itemType = c.getInt(indexItemType);
576           entry.cellX = c.getInt(indexCellX);
577           entry.cellY = c.getInt(indexCellY);
578           entry.spanX = c.getInt(indexSpanX);
579           entry.spanY = c.getInt(indexSpanY);
580           entry.screenId = screen;
581
582           try {
583               // calculate weight
584               switch (entry.itemType) {
585                   case Favorites.ITEM_TYPE_SHORTCUT:
586                   case Favorites.ITEM_TYPE_APPLICATION: {
587                       verifyIntent(c.getString(indexIntent));
588                       entry.weight = entry.itemType == Favorites.ITEM_TYPE_SHORTCUT
589                           ? WT_SHORTCUT : WT_APPLICATION;
590                       break;
591                   }
592                   case Favorites.ITEM_TYPE_APPWIDGET: {
593                       String provider = c.getString(indexAppWidgetProvider);
594                       ComponentName cn = ComponentName.unflattenFromString(provider);
595                       verifyPackage(cn.getPackageName());
596                       entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
597                               * entry.spanX * entry.spanY);
598
599                       // Migration happens for current user only.
600                       LauncherAppWidgetProviderInfo pInfo = LauncherModel.getProviderInfo(
601                               mContext, cn, UserHandleCompat.myUserHandle());
602                       Point spans = pInfo == null ?
603                               mWidgetMinSize.get(provider) : pInfo.getMinSpans(mIdp, mContext);
604                       if (spans != null) {
605                           entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
606                           entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
607                       } else {
608                           // Assume that the widget be resized down to 2x2
609                           entry.minSpanX = entry.minSpanY = 2;
610                       }
611
612                       if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
613                           throw new Exception("Widget can't be resized down to fit the grid");
614                       }
615                       break;
616                   }
617                   case Favorites.ITEM_TYPE_FOLDER: {
618                       int total = getFolderItemsCount(entry.id);
619                       if (total == 0) {
620                           throw new Exception("Folder is empty");
621                       }
622                       entry.weight = WT_FOLDER_FACTOR * total;
623                       break;
624                   }
625                   default:
626                       throw new Exception("Invalid item type");
627               }
628           } catch (Exception e) {
629               if (DEBUG) {
630                   Log.d(TAG, "Removing item " + entry.id, e);
631               }
632               mEntryToRemove.add(entry.id);
633               continue;
634           }
635
636           entries.add(entry);
637       }
638       return entries;
639    }
640
641    /**
642     * @return the number of valid items in the folder.
643     */
644    private int getFolderItemsCount(long folderId) {
645        Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
646                new String[] {Favorites._ID, Favorites.INTENT},
647                Favorites.CONTAINER + " = " + folderId, null, null, null);
648
649        int total = 0;
650        while (c.moveToNext()) {
651            try {
652                verifyIntent(c.getString(1));
653                total++;
654            } catch (Exception e) {
655                mEntryToRemove.add(c.getLong(0));
656            }
657        }
658
659        return total;
660    }
661
662    /**
663     * Verifies if the intent should be restored.
664     */
665    private void verifyIntent(String intentStr) throws Exception {
666        Intent intent = Intent.parseUri(intentStr, 0);
667        if (intent.getComponent() != null) {
668            verifyPackage(intent.getComponent().getPackageName());
669        } else if (intent.getPackage() != null) {
670            // Only verify package if the component was null.
671            verifyPackage(intent.getPackage());
672        }
673    }
674
675    /**
676     * Verifies if the package should be restored
677     */
678    private void verifyPackage(String packageName) throws Exception {
679        if (!mValidPackages.contains(packageName)) {
680            throw new Exception("Package not available");
681        }
682    }
683
684    private static class DbEntry extends ItemInfo implements Comparable<DbEntry> {
685
686        public float weight;
687
688        public DbEntry() { }
689
690        public DbEntry copy() {
691            DbEntry entry = new DbEntry();
692            entry.copyFrom(this);
693            entry.weight = weight;
694            entry.minSpanX = minSpanX;
695            entry.minSpanY = minSpanY;
696            return entry;
697        }
698
699        /**
700         * Comparator such that larger widgets come first,  followed by all 1x1 items
701         * based on their weights.
702         */
703        @Override
704        public int compareTo(DbEntry another) {
705            if (itemType == Favorites.ITEM_TYPE_APPWIDGET) {
706                if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
707                    return another.spanY * another.spanX - spanX * spanY;
708                } else {
709                    return -1;
710                }
711            } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
712                return 1;
713            } else {
714                // Place higher weight before lower weight.
715                return Float.compare(another.weight, weight);
716            }
717        }
718
719        public boolean columnsSame(DbEntry org) {
720            return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX &&
721                    org.spanY == spanY && org.screenId == screenId;
722        }
723
724        public void addToContentValues(ContentValues values) {
725            values.put(LauncherSettings.Favorites.SCREEN, screenId);
726            values.put(LauncherSettings.Favorites.CELLX, cellX);
727            values.put(LauncherSettings.Favorites.CELLY, cellY);
728            values.put(LauncherSettings.Favorites.SPANX, spanX);
729            values.put(LauncherSettings.Favorites.SPANY, spanY);
730        }
731    }
732
733    @Thunk static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
734        ArrayList<DbEntry> dup = new ArrayList<DbEntry>(src.size());
735        for (DbEntry e : src) {
736            dup.add(e.copy());
737        }
738        return dup;
739    }
740
741    private static Point parsePoint(String point) {
742        String[] split = point.split(",");
743        return new Point(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
744    }
745
746    public static void markForMigration(Context context, int srcX, int srcY,
747            HashSet<String> widgets) {
748        prefs(context).edit()
749                .putString(KEY_MIGRATION_SOURCE_SIZE, srcX + "," + srcY)
750                .putStringSet(KEY_MIGRATION_WIDGET_MINSIZE, widgets)
751                .apply();
752    }
753
754    public static boolean shouldRunTask(Context context) {
755        return !TextUtils.isEmpty(prefs(context).getString(KEY_MIGRATION_SOURCE_SIZE, ""));
756    }
757
758    public static void clearFlags(Context context) {
759        prefs(context).edit().remove(KEY_MIGRATION_SOURCE_SIZE)
760                .remove(KEY_MIGRATION_WIDGET_MINSIZE).commit();
761    }
762
763    private static SharedPreferences prefs(Context context) {
764        return context.getSharedPreferences(LauncherAppState.getSharedPreferencesKey(),
765                Context.MODE_PRIVATE);
766    }
767}
768