summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/model/GridSizeMigrationTask.java
blob: 289de1a4b187ffae99fc9cc6e601c5fe16da3bc9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
package com.android.launcher3.model;

import static com.android.launcher3.Utilities.getPointString;
import static com.android.launcher3.Utilities.parsePoint;

import android.content.ComponentName;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Point;
import android.net.Uri;
import android.util.Log;

import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.PackageInstallerCompat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.GridOccupancy;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;

/**
 * This class takes care of shrinking the workspace (by maximum of one row and one column), as a
 * result of restoring from a larger device or device density change.
 */
public class GridSizeMigrationTask {

    public static boolean ENABLED = Utilities.ATLEAST_NOUGAT;

    private static final String TAG = "GridSizeMigrationTask";
    private static final boolean DEBUG = true;

    private static final String KEY_MIGRATION_SRC_WORKSPACE_SIZE = "migration_src_workspace_size";
    private static final String KEY_MIGRATION_SRC_HOTSEAT_COUNT = "migration_src_hotseat_count";

    // These are carefully selected weights for various item types (Math.random?), to allow for
    // the least absurd migration experience.
    private static final float WT_SHORTCUT = 1;
    private static final float WT_APPLICATION = 0.8f;
    private static final float WT_WIDGET_MIN = 2;
    private static final float WT_WIDGET_FACTOR = 0.6f;
    private static final float WT_FOLDER_FACTOR = 0.5f;

    private final Context mContext;
    private final InvariantDeviceProfile mIdp;

    private final ContentValues mTempValues = new ContentValues();
    protected final IntArray mEntryToRemove = new IntArray();
    private final ArrayList<ContentProviderOperation> mUpdateOperations = new ArrayList<>();
    protected final ArrayList<DbEntry> mCarryOver = new ArrayList<>();
    private final HashSet<String> mValidPackages;

    private final int mSrcX, mSrcY;
    private final int mTrgX, mTrgY;
    private final boolean mShouldRemoveX, mShouldRemoveY;

    private final int mSrcHotseatSize;
    private final int mDestHotseatSize;

    protected GridSizeMigrationTask(Context context, InvariantDeviceProfile idp,
            HashSet<String> validPackages, Point sourceSize, Point targetSize) {
        mContext = context;
        mValidPackages = validPackages;
        mIdp = idp;

        mSrcX = sourceSize.x;
        mSrcY = sourceSize.y;

        mTrgX = targetSize.x;
        mTrgY = targetSize.y;

        mShouldRemoveX = mTrgX < mSrcX;
        mShouldRemoveY = mTrgY < mSrcY;

        // Non-used variables
        mSrcHotseatSize = mDestHotseatSize = -1;
    }

    protected GridSizeMigrationTask(Context context,
            InvariantDeviceProfile idp, HashSet<String> validPackages,
            int srcHotseatSize, int destHotseatSize) {
        mContext = context;
        mIdp = idp;
        mValidPackages = validPackages;

        mSrcHotseatSize = srcHotseatSize;

        mDestHotseatSize = destHotseatSize;

        // Non-used variables
        mSrcX = mSrcY = mTrgX = mTrgY = -1;
        mShouldRemoveX = mShouldRemoveY = false;
    }

    /**
     * Applied all the pending DB operations
     *
     * @return true if any DB operation was commited.
     */
    private boolean applyOperations() throws Exception {
        // Update items
        if (!mUpdateOperations.isEmpty()) {
            mContext.getContentResolver().applyBatch(LauncherProvider.AUTHORITY, mUpdateOperations);
        }

        if (!mEntryToRemove.isEmpty()) {
            if (DEBUG) {
                Log.d(TAG, "Removing items: " + mEntryToRemove.toConcatString());
            }
            mContext.getContentResolver().delete(LauncherSettings.Favorites.CONTENT_URI,
                    Utilities.createDbSelectionQuery(
                            LauncherSettings.Favorites._ID, mEntryToRemove), null);
        }

        return !mUpdateOperations.isEmpty() || !mEntryToRemove.isEmpty();
    }

    /**
     * To migrate hotseat, we load all the entries in order (LTR or RTL) and arrange them
     * in the order in the new hotseat while keeping an empty space for all-apps. If the number of
     * entries is more than what can fit in the new hotseat, we drop the entries with least weight.
     * For weight calculation {@see #WT_SHORTCUT}, {@see #WT_APPLICATION}
     * & {@see #WT_FOLDER_FACTOR}.
     *
     * @return true if any DB change was made
     */
    protected boolean migrateHotseat() throws Exception {
        ArrayList<DbEntry> items = loadHotseatEntries();
        while (items.size() > mDestHotseatSize) {
            // Pick the center item by default.
            DbEntry toRemove = items.get(items.size() / 2);

            // Find the item with least weight.
            for (DbEntry entry : items) {
                if (entry.weight < toRemove.weight) {
                    toRemove = entry;
                }
            }

            mEntryToRemove.add(toRemove.id);
            items.remove(toRemove);
        }

        // Update screen IDS
        int newScreenId = 0;
        for (DbEntry entry : items) {
            if (entry.screenId != newScreenId) {
                entry.screenId = newScreenId;

                // These values does not affect the item position, but we should set them
                // to something other than -1.
                entry.cellX = newScreenId;
                entry.cellY = 0;

                update(entry);
            }

            newScreenId++;
        }

        return applyOperations();
    }

    /**
     * @return true if any DB change was made
     */
    protected boolean migrateWorkspace() throws Exception {
        IntArray allScreens = LauncherModel.loadWorkspaceScreensDb(mContext);
        if (allScreens.isEmpty()) {
            throw new Exception("Unable to get workspace screens");
        }

        for (int i = 0; i < allScreens.size(); i++) {
            int screenId = allScreens.get(i);
            if (DEBUG) {
                Log.d(TAG, "Migrating " + screenId);
            }
            migrateScreen(screenId);
        }

        if (!mCarryOver.isEmpty()) {
            IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
            for (DbEntry e : mCarryOver) {
                itemMap.put(e.id, e);
            }

            do {
                // Some items are still remaining. Try adding a few new screens.

                // At every iteration, make sure that at least one item is removed from
                // {@link #mCarryOver}, to prevent an infinite loop. If no item could be removed,
                // break the loop and abort migration by throwing an exception.
                OptimalPlacementSolution placement = new OptimalPlacementSolution(
                        new GridOccupancy(mTrgX, mTrgY), deepCopy(mCarryOver), 0, true);
                placement.find();
                if (placement.finalPlacedItems.size() > 0) {
                    int newScreenId = LauncherSettings.Settings.call(
                            mContext.getContentResolver(),
                            LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
                            .getInt(LauncherSettings.Settings.EXTRA_VALUE);

                    allScreens.add(newScreenId);
                    for (DbEntry item : placement.finalPlacedItems) {
                        if (!mCarryOver.remove(itemMap.get(item.id))) {
                            throw new Exception("Unable to find matching items");
                        }
                        item.screenId = newScreenId;
                        update(item);
                    }
                } else {
                    throw new Exception("None of the items can be placed on an empty screen");
                }

            } while (!mCarryOver.isEmpty());

            // Update screens
            final Uri uri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
            mUpdateOperations.add(ContentProviderOperation.newDelete(uri).build());
            int count = allScreens.size();
            for (int i = 0; i < count; i++) {
                ContentValues v = new ContentValues();
                int screenId = allScreens.get(i);
                v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
                v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
                mUpdateOperations.add(ContentProviderOperation.newInsert(uri).withValues(
                        v).build());
            }
        }
        return applyOperations();
    }

    /**
     * Migrate a particular screen id.
     * Strategy:
     *   1) For all possible combinations of row and column, pick the one which causes the least
     *      data loss: {@link #tryRemove(int, int, int, ArrayList, float[])}
     *   2) Maintain a list of all lost items before this screen, and add any new item lost from
     *      this screen to that list as well.
     *   3) If all those items from the above list can be placed on this screen, place them
     *      (otherwise they are placed on a new screen).
     */
    protected void migrateScreen(int screenId) {
        // If we are migrating the first screen, do not touch the first row.
        int startY =
                (FeatureFlags.QSB_ON_FIRST_SCREEN.get() && screenId == Workspace.FIRST_SCREEN_ID)
                ? 1 : 0;

        ArrayList<DbEntry> items = loadWorkspaceEntries(screenId);

        int removedCol = Integer.MAX_VALUE;
        int removedRow = Integer.MAX_VALUE;

        // removeWt represents the cost function for loss of items during migration, and moveWt
        // represents the cost function for repositioning the items. moveWt is only considered if
        // removeWt is same for two different configurations.
        // Start with Float.MAX_VALUE (assuming full data) and pick the configuration with least
        // cost.
        float removeWt = Float.MAX_VALUE;
        float moveWt = Float.MAX_VALUE;
        float[] outLoss = new float[2];
        ArrayList<DbEntry> finalItems = null;

        // Try removing all possible combinations
        for (int x = 0; x < mSrcX; x++) {
            // Try removing the rows first from bottom. This keeps the workspace
            // nicely aligned with hotseat.
            for (int y = mSrcY - 1; y >= startY; y--) {
                // Use a deep copy when trying out a particular combination as it can change
                // the underlying object.
                ArrayList<DbEntry> itemsOnScreen = tryRemove(x, y, startY, deepCopy(items),
                        outLoss);

                if ((outLoss[0] < removeWt) || ((outLoss[0] == removeWt) && (outLoss[1]
                        < moveWt))) {
                    removeWt = outLoss[0];
                    moveWt = outLoss[1];
                    removedCol = mShouldRemoveX ? x : removedCol;
                    removedRow = mShouldRemoveY ? y : removedRow;
                    finalItems = itemsOnScreen;
                }

                // No need to loop over all rows, if a row removal is not needed.
                if (!mShouldRemoveY) {
                    break;
                }
            }

            if (!mShouldRemoveX) {
                break;
            }
        }

        if (DEBUG) {
            Log.d(TAG, String.format("Removing row %d, column %d on screen %d",
                    removedRow, removedCol, screenId));
        }

        IntSparseArrayMap<DbEntry> itemMap = new IntSparseArrayMap<>();
        for (DbEntry e : deepCopy(items)) {
            itemMap.put(e.id, e);
        }

        for (DbEntry item : finalItems) {
            DbEntry org = itemMap.get(item.id);
            itemMap.remove(item.id);

            // Check if update is required
            if (!item.columnsSame(org)) {
                update(item);
            }
        }

        // The remaining items in {@link #itemMap} are those which didn't get placed.
        for (DbEntry item : itemMap) {
            mCarryOver.add(item);
        }

        if (!mCarryOver.isEmpty() && removeWt == 0) {
            // No new items were removed in this step. Try placing all the items on this screen.
            GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
            occupied.markCells(0, 0, mTrgX, startY, true);
            for (DbEntry item : finalItems) {
                occupied.markCells(item, true);
            }

            OptimalPlacementSolution placement = new OptimalPlacementSolution(occupied,
                    deepCopy(mCarryOver), startY, true);
            placement.find();
            if (placement.lowestWeightLoss == 0) {
                // All items got placed

                for (DbEntry item : placement.finalPlacedItems) {
                    item.screenId = screenId;
                    update(item);
                }

                mCarryOver.clear();
            }
        }
    }

    /**
     * Updates an item in the DB.
     */
    protected void update(DbEntry item) {
        mTempValues.clear();
        item.addToContentValues(mTempValues);
        mUpdateOperations.add(ContentProviderOperation
                .newUpdate(LauncherSettings.Favorites.getContentUri(item.id))
                .withValues(mTempValues).build());
    }

    /**
     * Tries the remove the provided row and column.
     *
     * @param items all the items on the screen under operation
     * @param outLoss array of size 2. The first entry is filled with weight loss, and the second
     * with the overall item movement.
     */
    private ArrayList<DbEntry> tryRemove(int col, int row, int startY,
            ArrayList<DbEntry> items, float[] outLoss) {
        GridOccupancy occupied = new GridOccupancy(mTrgX, mTrgY);
        occupied.markCells(0, 0, mTrgX, startY, true);

        col = mShouldRemoveX ? col : Integer.MAX_VALUE;
        row = mShouldRemoveY ? row : Integer.MAX_VALUE;

        ArrayList<DbEntry> finalItems = new ArrayList<>();
        ArrayList<DbEntry> removedItems = new ArrayList<>();

        for (DbEntry item : items) {
            if ((item.cellX <= col && (item.spanX + item.cellX) > col)
                || (item.cellY <= row && (item.spanY + item.cellY) > row)) {
                removedItems.add(item);
                if (item.cellX >= col) item.cellX --;
                if (item.cellY >= row) item.cellY --;
            } else {
                if (item.cellX > col) item.cellX --;
                if (item.cellY > row) item.cellY --;
                finalItems.add(item);
                occupied.markCells(item, true);
            }
        }

        OptimalPlacementSolution placement =
                new OptimalPlacementSolution(occupied, removedItems, startY);
        placement.find();
        finalItems.addAll(placement.finalPlacedItems);
        outLoss[0] = placement.lowestWeightLoss;
        outLoss[1] = placement.lowestMoveCost;
        return finalItems;
    }

    private class OptimalPlacementSolution {
        private final ArrayList<DbEntry> itemsToPlace;
        private final GridOccupancy occupied;

        // If set to true, item movement are not considered in move cost, leading to a more
        // linear placement.
        private final boolean ignoreMove;

        // The first row in the grid from where the placement should start.
        private final int startY;

        float lowestWeightLoss = Float.MAX_VALUE;
        float lowestMoveCost = Float.MAX_VALUE;
        ArrayList<DbEntry> finalPlacedItems;

        public OptimalPlacementSolution(
                GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace, int startY) {
            this(occupied, itemsToPlace, startY, false);
        }

        public OptimalPlacementSolution(GridOccupancy occupied, ArrayList<DbEntry> itemsToPlace,
                int startY, boolean ignoreMove) {
            this.occupied = occupied;
            this.itemsToPlace = itemsToPlace;
            this.ignoreMove = ignoreMove;
            this.startY = startY;

            // Sort the items such that larger widgets appear first followed by 1x1 items
            Collections.sort(this.itemsToPlace);
        }

        public void find() {
            find(0, 0, 0, new ArrayList<DbEntry>());
        }

        /**
         * Recursively finds a placement for the provided items.
         *
         * @param index the position in {@link #itemsToPlace} to start looking at.
         * @param weightLoss total weight loss upto this point
         * @param moveCost total move cost upto this point
         * @param itemsPlaced all the items already placed upto this point
         */
        public void find(int index, float weightLoss, float moveCost,
                ArrayList<DbEntry> itemsPlaced) {
            if ((weightLoss >= lowestWeightLoss) ||
                    ((weightLoss == lowestWeightLoss) && (moveCost >= lowestMoveCost))) {
                // Abort, as we already have a better solution.
                return;

            } else if (index >= itemsToPlace.size()) {
                // End loop.
                lowestWeightLoss = weightLoss;
                lowestMoveCost = moveCost;

                // Keep a deep copy of current configuration as it can change during recursion.
                finalPlacedItems = deepCopy(itemsPlaced);
                return;
            }

            DbEntry me = itemsToPlace.get(index);
            int myX = me.cellX;
            int myY = me.cellY;

            // List of items to pass over if this item was placed.
            ArrayList<DbEntry> itemsIncludingMe = new ArrayList<>(itemsPlaced.size() + 1);
            itemsIncludingMe.addAll(itemsPlaced);
            itemsIncludingMe.add(me);

            if (me.spanX > 1 || me.spanY > 1) {
                // If the current item is a widget (and it greater than 1x1), try to place it at
                // all possible positions. This is because a widget placed at one position can
                // affect the placement of a different widget.
                int myW = me.spanX;
                int myH = me.spanY;

                for (int y = startY; y < mTrgY; y++) {
                    for (int x = 0; x < mTrgX; x++) {
                        float newMoveCost = moveCost;
                        if (x != myX) {
                            me.cellX = x;
                            newMoveCost ++;
                        }
                        if (y != myY) {
                            me.cellY = y;
                            newMoveCost ++;
                        }
                        if (ignoreMove) {
                            newMoveCost = moveCost;
                        }

                        if (occupied.isRegionVacant(x, y, myW, myH)) {
                            // place at this position and continue search.
                            occupied.markCells(me, true);
                            find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
                            occupied.markCells(me, false);
                        }

                        // Try resizing horizontally
                        if (myW > me.minSpanX && occupied.isRegionVacant(x, y, myW - 1, myH)) {
                            me.spanX --;
                            occupied.markCells(me, true);
                            // 1 extra move cost
                            find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                            occupied.markCells(me, false);
                            me.spanX ++;
                        }

                        // Try resizing vertically
                        if (myH > me.minSpanY && occupied.isRegionVacant(x, y, myW, myH - 1)) {
                            me.spanY --;
                            occupied.markCells(me, true);
                            // 1 extra move cost
                            find(index + 1, weightLoss, newMoveCost + 1, itemsIncludingMe);
                            occupied.markCells(me, false);
                            me.spanY ++;
                        }

                        // Try resizing horizontally & vertically
                        if (myH > me.minSpanY && myW > me.minSpanX &&
                                occupied.isRegionVacant(x, y, myW - 1, myH - 1)) {
                            me.spanX --;
                            me.spanY --;
                            occupied.markCells(me, true);
                            // 2 extra move cost
                            find(index + 1, weightLoss, newMoveCost + 2, itemsIncludingMe);
                            occupied.markCells(me, false);
                            me.spanX ++;
                            me.spanY ++;
                        }
                        me.cellX = myX;
                        me.cellY = myY;
                    }
                }

                // Finally also try a solution when this item is not included. Trying it in the end
                // causes it to get skipped in most cases due to higher weight loss, and prevents
                // unnecessary deep copies of various configurations.
                find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
            } else {
                // Since this is a 1x1 item and all the following items are also 1x1, just place
                // it at 'the most appropriate position' and hope for the best.
                // The most appropriate position: one with lease straight line distance
                int newDistance = Integer.MAX_VALUE;
                int newX = Integer.MAX_VALUE, newY = Integer.MAX_VALUE;

                for (int y = startY; y < mTrgY; y++) {
                    for (int x = 0; x < mTrgX; x++) {
                        if (!occupied.cells[x][y]) {
                            int dist = ignoreMove ? 0 :
                                    ((me.cellX - x) * (me.cellX - x) + (me.cellY - y) * (me.cellY
                                            - y));
                            if (dist < newDistance) {
                                newX = x;
                                newY = y;
                                newDistance = dist;
                            }
                        }
                    }
                }

                if (newX < mTrgX && newY < mTrgY) {
                    float newMoveCost = moveCost;
                    if (newX != myX) {
                        me.cellX = newX;
                        newMoveCost ++;
                    }
                    if (newY != myY) {
                        me.cellY = newY;
                        newMoveCost ++;
                    }
                    if (ignoreMove) {
                        newMoveCost = moveCost;
                    }
                    occupied.markCells(me, true);
                    find(index + 1, weightLoss, newMoveCost, itemsIncludingMe);
                    occupied.markCells(me, false);
                    me.cellX = myX;
                    me.cellY = myY;

                    // Try to find a solution without this item, only if
                    //  1) there was at least one space, i.e., we were able to place this item
                    //  2) if the next item has the same weight (all items are already sorted), as
                    //     if it has lower weight, that solution will automatically get discarded.
                    //  3) ignoreMove false otherwise, move cost is ignored and the weight will
                    //      anyway be same.
                    if (index + 1 < itemsToPlace.size()
                            && itemsToPlace.get(index + 1).weight >= me.weight && !ignoreMove) {
                        find(index + 1, weightLoss + me.weight, moveCost, itemsPlaced);
                    }
                } else {
                    // No more space. Jump to the end.
                    for (int i = index + 1; i < itemsToPlace.size(); i++) {
                        weightLoss += itemsToPlace.get(i).weight;
                    }
                    find(itemsToPlace.size(), weightLoss + me.weight, moveCost, itemsPlaced);
                }
            }
        }
    }

    private ArrayList<DbEntry> loadHotseatEntries() {
        Cursor c =  mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
                new String[]{
                        Favorites._ID,                  // 0
                        Favorites.ITEM_TYPE,            // 1
                        Favorites.INTENT,               // 2
                        Favorites.SCREEN},              // 3
                Favorites.CONTAINER + " = " + Favorites.CONTAINER_HOTSEAT, null, null, null);

        final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
        final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
        final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
        final int indexScreen = c.getColumnIndexOrThrow(Favorites.SCREEN);

        ArrayList<DbEntry> entries = new ArrayList<>();
        while (c.moveToNext()) {
            DbEntry entry = new DbEntry();
            entry.id = c.getInt(indexId);
            entry.itemType = c.getInt(indexItemType);
            entry.screenId = c.getInt(indexScreen);

            if (entry.screenId >= mSrcHotseatSize) {
                mEntryToRemove.add(entry.id);
                continue;
            }

            try {
                // calculate weight
                switch (entry.itemType) {
                    case Favorites.ITEM_TYPE_SHORTCUT:
                    case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                    case Favorites.ITEM_TYPE_APPLICATION: {
                        verifyIntent(c.getString(indexIntent));
                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
                                WT_APPLICATION : WT_SHORTCUT;
                        break;
                    }
                    case Favorites.ITEM_TYPE_FOLDER: {
                        int total = getFolderItemsCount(entry.id);
                        if (total == 0) {
                            throw new Exception("Folder is empty");
                        }
                        entry.weight = WT_FOLDER_FACTOR * total;
                        break;
                    }
                    default:
                        throw new Exception("Invalid item type");
                }
            } catch (Exception e) {
                if (DEBUG) {
                    Log.d(TAG, "Removing item " + entry.id, e);
                }
                mEntryToRemove.add(entry.id);
                continue;
            }
            entries.add(entry);
        }
        c.close();
        return entries;
    }


    /**
     * Loads entries for a particular screen id.
     */
    protected ArrayList<DbEntry> loadWorkspaceEntries(int screen) {
        Cursor c = queryWorkspace(
                new String[]{
                        Favorites._ID,                  // 0
                        Favorites.ITEM_TYPE,            // 1
                        Favorites.CELLX,                // 2
                        Favorites.CELLY,                // 3
                        Favorites.SPANX,                // 4
                        Favorites.SPANY,                // 5
                        Favorites.INTENT,               // 6
                        Favorites.APPWIDGET_PROVIDER,   // 7
                        Favorites.APPWIDGET_ID},        // 8
                Favorites.CONTAINER + " = " + Favorites.CONTAINER_DESKTOP
                        + " AND " + Favorites.SCREEN + " = " + screen);

        final int indexId = c.getColumnIndexOrThrow(Favorites._ID);
        final int indexItemType = c.getColumnIndexOrThrow(Favorites.ITEM_TYPE);
        final int indexCellX = c.getColumnIndexOrThrow(Favorites.CELLX);
        final int indexCellY = c.getColumnIndexOrThrow(Favorites.CELLY);
        final int indexSpanX = c.getColumnIndexOrThrow(Favorites.SPANX);
        final int indexSpanY = c.getColumnIndexOrThrow(Favorites.SPANY);
        final int indexIntent = c.getColumnIndexOrThrow(Favorites.INTENT);
        final int indexAppWidgetProvider = c.getColumnIndexOrThrow(Favorites.APPWIDGET_PROVIDER);
        final int indexAppWidgetId = c.getColumnIndexOrThrow(Favorites.APPWIDGET_ID);

        ArrayList<DbEntry> entries = new ArrayList<>();
        while (c.moveToNext()) {
            DbEntry entry = new DbEntry();
            entry.id = c.getInt(indexId);
            entry.itemType = c.getInt(indexItemType);
            entry.cellX = c.getInt(indexCellX);
            entry.cellY = c.getInt(indexCellY);
            entry.spanX = c.getInt(indexSpanX);
            entry.spanY = c.getInt(indexSpanY);
            entry.screenId = screen;

            try {
                // calculate weight
                switch (entry.itemType) {
                    case Favorites.ITEM_TYPE_SHORTCUT:
                    case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                    case Favorites.ITEM_TYPE_APPLICATION: {
                        verifyIntent(c.getString(indexIntent));
                        entry.weight = entry.itemType == Favorites.ITEM_TYPE_APPLICATION ?
                                WT_APPLICATION : WT_SHORTCUT;
                        break;
                    }
                    case Favorites.ITEM_TYPE_APPWIDGET: {
                        String provider = c.getString(indexAppWidgetProvider);
                        ComponentName cn = ComponentName.unflattenFromString(provider);
                        verifyPackage(cn.getPackageName());
                        entry.weight = Math.max(WT_WIDGET_MIN, WT_WIDGET_FACTOR
                                * entry.spanX * entry.spanY);

                        int widgetId = c.getInt(indexAppWidgetId);
                        LauncherAppWidgetProviderInfo pInfo = AppWidgetManagerCompat.getInstance(
                                mContext).getLauncherAppWidgetInfo(widgetId);
                        Point spans = null;
                        if (pInfo != null) {
                            spans = pInfo.getMinSpans();
                        }
                        if (spans != null) {
                            entry.minSpanX = spans.x > 0 ? spans.x : entry.spanX;
                            entry.minSpanY = spans.y > 0 ? spans.y : entry.spanY;
                        } else {
                            // Assume that the widget be resized down to 2x2
                            entry.minSpanX = entry.minSpanY = 2;
                        }

                        if (entry.minSpanX > mTrgX || entry.minSpanY > mTrgY) {
                            throw new Exception("Widget can't be resized down to fit the grid");
                        }
                        break;
                    }
                    case Favorites.ITEM_TYPE_FOLDER: {
                        int total = getFolderItemsCount(entry.id);
                        if (total == 0) {
                            throw new Exception("Folder is empty");
                        }
                        entry.weight = WT_FOLDER_FACTOR * total;
                        break;
                    }
                    default:
                        throw new Exception("Invalid item type");
                }
            } catch (Exception e) {
                if (DEBUG) {
                    Log.d(TAG, "Removing item " + entry.id, e);
                }
                mEntryToRemove.add(entry.id);
                continue;
            }
            entries.add(entry);
        }
        c.close();
        return entries;
    }

    /**
     * @return the number of valid items in the folder.
     */
    private int getFolderItemsCount(int folderId) {
        Cursor c = queryWorkspace(
                new String[]{Favorites._ID, Favorites.INTENT},
                Favorites.CONTAINER + " = " + folderId);

        int total = 0;
        while (c.moveToNext()) {
            try {
                verifyIntent(c.getString(1));
                total++;
            } catch (Exception e) {
                mEntryToRemove.add(c.getInt(0));
            }
        }
        c.close();
        return total;
    }

    protected Cursor queryWorkspace(String[] columns, String where) {
        return mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
                columns, where, null, null, null);
    }

    /**
     * Verifies if the intent should be restored.
     */
    private void verifyIntent(String intentStr) throws Exception {
        Intent intent = Intent.parseUri(intentStr, 0);
        if (intent.getComponent() != null) {
            verifyPackage(intent.getComponent().getPackageName());
        } else if (intent.getPackage() != null) {
            // Only verify package if the component was null.
            verifyPackage(intent.getPackage());
        }
    }

    /**
     * Verifies if the package should be restored
     */
    private void verifyPackage(String packageName) throws Exception {
        if (!mValidPackages.contains(packageName)) {
            throw new Exception("Package not available");
        }
    }

    protected static class DbEntry extends ItemInfo implements Comparable<DbEntry> {

        public float weight;

        public DbEntry() {
        }

        public DbEntry copy() {
            DbEntry entry = new DbEntry();
            entry.copyFrom(this);
            entry.weight = weight;
            entry.minSpanX = minSpanX;
            entry.minSpanY = minSpanY;
            return entry;
        }

        /**
         * Comparator such that larger widgets come first,  followed by all 1x1 items
         * based on their weights.
         */
        @Override
        public int compareTo(DbEntry another) {
            if (itemType == Favorites.ITEM_TYPE_APPWIDGET) {
                if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
                    return another.spanY * another.spanX - spanX * spanY;
                } else {
                    return -1;
                }
            } else if (another.itemType == Favorites.ITEM_TYPE_APPWIDGET) {
                return 1;
            } else {
                // Place higher weight before lower weight.
                return Float.compare(another.weight, weight);
            }
        }

        public boolean columnsSame(DbEntry org) {
            return org.cellX == cellX && org.cellY == cellY && org.spanX == spanX &&
                    org.spanY == spanY && org.screenId == screenId;
        }

        public void addToContentValues(ContentValues values) {
            values.put(LauncherSettings.Favorites.SCREEN, screenId);
            values.put(LauncherSettings.Favorites.CELLX, cellX);
            values.put(LauncherSettings.Favorites.CELLY, cellY);
            values.put(LauncherSettings.Favorites.SPANX, spanX);
            values.put(LauncherSettings.Favorites.SPANY, spanY);
        }
    }

    private static ArrayList<DbEntry> deepCopy(ArrayList<DbEntry> src) {
        ArrayList<DbEntry> dup = new ArrayList<>(src.size());
        for (DbEntry e : src) {
            dup.add(e.copy());
        }
        return dup;
    }

    public static void markForMigration(
            Context context, int gridX, int gridY, int hotseatSize) {
        Utilities.getPrefs(context).edit()
                .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, getPointString(gridX, gridY))
                .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, hotseatSize)
                .apply();
    }

    /**
     * Migrates the workspace and hotseat in case their sizes changed.
     *
     * @return false if the migration failed.
     */
    public static boolean migrateGridIfNeeded(Context context) {
        SharedPreferences prefs = Utilities.getPrefs(context);
        InvariantDeviceProfile idp = LauncherAppState.getIDP(context);

        String gridSizeString = getPointString(idp.numColumns, idp.numRows);

        if (gridSizeString.equals(prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, "")) &&
                idp.numHotseatIcons == prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
                        idp.numHotseatIcons)) {
            // Skip if workspace and hotseat sizes have not changed.
            return true;
        }

        long migrationStartTime = System.currentTimeMillis();
        try {
            boolean dbChanged = false;

            HashSet<String> validPackages = getValidPackages(context);
            // Hotseat
            int srcHotseatCount = prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT,
                    idp.numHotseatIcons);
            if (srcHotseatCount != idp.numHotseatIcons) {
                // Migrate hotseat.

                dbChanged = new GridSizeMigrationTask(context, LauncherAppState.getIDP(context),
                        validPackages, srcHotseatCount, idp.numHotseatIcons).migrateHotseat();
            }

            // Grid size
            Point targetSize = new Point(idp.numColumns, idp.numRows);
            Point sourceSize = parsePoint(prefs.getString(
                    KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString));

            if (new MultiStepMigrationTask(validPackages, context).migrate(sourceSize,
                    targetSize)) {
                dbChanged = true;
            }

            if (dbChanged) {
                // Make sure we haven't removed everything.
                final Cursor c = context.getContentResolver().query(
                        LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
                boolean hasData = c.moveToNext();
                c.close();
                if (!hasData) {
                    throw new Exception("Removed every thing during grid resize");
                }
            }

            return true;
        } catch (Exception e) {
            Log.e(TAG, "Error during grid migration", e);

            return false;
        } finally {
            Log.v(TAG, "Workspace migration completed in "
                    + (System.currentTimeMillis() - migrationStartTime));

            // Save current configuration, so that the migration does not run again.
            prefs.edit()
                    .putString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, gridSizeString)
                    .putInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, idp.numHotseatIcons)
                    .apply();
        }
    }

    protected static HashSet<String> getValidPackages(Context context) {
        // Initialize list of valid packages. This contain all the packages which are already on
        // the device and packages which are being installed. Any item which doesn't belong to
        // this set is removed.
        // Since the loader removes such items anyway, removing these items here doesn't cause
        // any extra data loss and gives us more free space on the grid for better migration.
        HashSet<String> validPackages = new HashSet<>();
        for (PackageInfo info : context.getPackageManager()
                .getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
            validPackages.add(info.packageName);
        }
        validPackages.addAll(PackageInstallerCompat.getInstance(context)
                .updateAndGetActiveSessionCache().keySet());
        return validPackages;
    }

    /**
     * Removes any broken item from the hotseat.
     *
     * @return a map with occupied hotseat position set to non-null value.
     */
    public static IntSparseArrayMap<Object> removeBrokenHotseatItems(Context context)
            throws Exception {
        GridSizeMigrationTask task = new GridSizeMigrationTask(
                context, LauncherAppState.getIDP(context), getValidPackages(context),
                Integer.MAX_VALUE, Integer.MAX_VALUE);

        // Load all the valid entries
        ArrayList<DbEntry> items = task.loadHotseatEntries();
        // Delete any entry marked for deletion by above load.
        task.applyOperations();
        IntSparseArrayMap<Object> positions = new IntSparseArrayMap<>();
        for (DbEntry item : items) {
            positions.put(item.screenId, item);
        }
        return positions;
    }

    /**
     * Task to run grid migration in multiple steps when the size difference is more than 1.
     */
    protected static class MultiStepMigrationTask {
        private final HashSet<String> mValidPackages;
        private final Context mContext;

        public MultiStepMigrationTask(HashSet<String> validPackages, Context context) {
            mValidPackages = validPackages;
            mContext = context;
        }

        public boolean migrate(Point sourceSize, Point targetSize) throws Exception {
            boolean dbChanged = false;
            if (!targetSize.equals(sourceSize)) {
                if (sourceSize.x < targetSize.x) {
                    // Source is smaller that target, just expand the grid without actual migration.
                    sourceSize.x = targetSize.x;
                }
                if (sourceSize.y < targetSize.y) {
                    // Source is smaller that target, just expand the grid without actual migration.
                    sourceSize.y = targetSize.y;
                }

                // Migrate the workspace grid, such that the points differ by max 1 in x and y
                // each on every step.
                while (!targetSize.equals(sourceSize)) {
                    // Get the next size, such that the points differ by max 1 in x and y each
                    Point nextSize = new Point(sourceSize);
                    if (targetSize.x < nextSize.x) {
                        nextSize.x--;
                    }
                    if (targetSize.y < nextSize.y) {
                        nextSize.y--;
                    }
                    if (runStepTask(sourceSize, nextSize)) {
                        dbChanged = true;
                    }
                    sourceSize.set(nextSize.x, nextSize.y);
                }
            }
            return dbChanged;
        }

        protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
            return new GridSizeMigrationTask(mContext, LauncherAppState.getIDP(mContext),
                    mValidPackages, sourceSize, nextSize).migrateWorkspace();
        }
    }
}