1package com.android.launcher3.model;
2
3import android.content.ContentValues;
4import android.content.Intent;
5import android.database.Cursor;
6import android.graphics.Point;
7import android.test.ProviderTestCase2;
8import android.test.suitebuilder.annotation.MediumTest;
9
10import com.android.launcher3.InvariantDeviceProfile;
11import com.android.launcher3.LauncherModel;
12import com.android.launcher3.LauncherProvider;
13import com.android.launcher3.LauncherSettings;
14import com.android.launcher3.config.FeatureFlags;
15import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask;
16import com.android.launcher3.util.TestLauncherProvider;
17
18import java.util.ArrayList;
19import java.util.HashSet;
20import java.util.LinkedList;
21
22/**
23 * Unit tests for {@link GridSizeMigrationTask}
24 */
25@MediumTest
26public class GridSizeMigrationTaskTest extends ProviderTestCase2<TestLauncherProvider> {
27
28    private static final long DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP;
29    private static final long HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
30
31    private static final int APPLICATION = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
32    private static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
33
34    private static final String TEST_PACKAGE = "com.android.launcher3.validpackage";
35    private static final String VALID_INTENT =
36            new Intent(Intent.ACTION_MAIN).setPackage(TEST_PACKAGE).toUri(0);
37
38    private HashSet<String> mValidPackages;
39    private InvariantDeviceProfile mIdp;
40
41    public GridSizeMigrationTaskTest() {
42        super(TestLauncherProvider.class, LauncherProvider.AUTHORITY);
43    }
44
45    @Override
46    protected void setUp() throws Exception {
47        super.setUp();
48        mValidPackages = new HashSet<>();
49        mValidPackages.add(TEST_PACKAGE);
50
51        mIdp = new InvariantDeviceProfile();
52    }
53
54    public void testHotseatMigration_apps_dropped() throws Exception {
55        long[] hotseatItems = {
56                addItem(APPLICATION, 0, HOTSEAT, 0, 0),
57                addItem(SHORTCUT, 1, HOTSEAT, 0, 0),
58                -1,
59                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
60                addItem(APPLICATION, 4, HOTSEAT, 0, 0),
61        };
62
63        mIdp.numHotseatIcons = 3;
64        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 3)
65                .migrateHotseat();
66        if (FeatureFlags.NO_ALL_APPS_ICON) {
67            // First item is dropped as it has the least weight.
68            verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
69        } else {
70            // First & last items are dropped as they have the least weight.
71            verifyHotseat(hotseatItems[1], -1, hotseatItems[3]);
72        }
73    }
74
75    public void testHotseatMigration_shortcuts_dropped() throws Exception {
76        long[] hotseatItems = {
77                addItem(APPLICATION, 0, HOTSEAT, 0, 0),
78                addItem(30, 1, HOTSEAT, 0, 0),
79                -1,
80                addItem(SHORTCUT, 3, HOTSEAT, 0, 0),
81                addItem(10, 4, HOTSEAT, 0, 0),
82        };
83
84        mIdp.numHotseatIcons = 3;
85        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages, 5, 3)
86                .migrateHotseat();
87        if (FeatureFlags.NO_ALL_APPS_ICON) {
88            // First item is dropped as it has the least weight.
89            verifyHotseat(hotseatItems[1], hotseatItems[3], hotseatItems[4]);
90        } else {
91            // First & third items are dropped as they have the least weight.
92            verifyHotseat(hotseatItems[1], -1, hotseatItems[4]);
93        }
94    }
95
96    private void verifyHotseat(long... sortedIds) {
97        int screenId = 0;
98        int total = 0;
99
100        for (long id : sortedIds) {
101            Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
102                    new String[]{LauncherSettings.Favorites._ID},
103                    "container=-101 and screen=" + screenId, null, null, null);
104
105            if (id == -1) {
106                assertEquals(0, c.getCount());
107            } else {
108                assertEquals(1, c.getCount());
109                c.moveToNext();
110                assertEquals(id, c.getLong(0));
111                total ++;
112            }
113            c.close();
114
115            screenId++;
116        }
117
118        // Verify that not other entry exist in the DB.
119        Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
120                new String[]{LauncherSettings.Favorites._ID},
121                "container=-101", null, null, null);
122        assertEquals(total, c.getCount());
123        c.close();
124    }
125
126    public void testWorkspace_empty_row_column_removed() throws Exception {
127        long[][][] ids = createGrid(new int[][][]{{
128                {  0,  0, -1,  1},
129                {  3,  1, -1,  4},
130                { -1, -1, -1, -1},
131                {  5,  2, -1,  6},
132        }});
133
134        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
135                new Point(4, 4), new Point(3, 3)).migrateWorkspace();
136
137        // Column 2 and row 2 got removed.
138        verifyWorkspace(new long[][][] {{
139                {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
140                {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
141                {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
142        }});
143    }
144
145    public void testWorkspace_new_screen_created() throws Exception {
146        long[][][] ids = createGrid(new int[][][]{{
147                {  0,  0,  0,  1},
148                {  3,  1,  0,  4},
149                { -1, -1, -1, -1},
150                {  5,  2, -1,  6},
151        }});
152
153        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
154                new Point(4, 4), new Point(3, 3)).migrateWorkspace();
155
156        // Items in the second column get moved to new screen
157        verifyWorkspace(new long[][][] {{
158                {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
159                {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
160                {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
161        }, {
162                {ids[0][0][2], ids[0][1][2], -1},
163        }});
164    }
165
166    public void testWorkspace_items_merged_in_next_screen() throws Exception {
167        long[][][] ids = createGrid(new int[][][]{{
168                {  0,  0,  0,  1},
169                {  3,  1,  0,  4},
170                { -1, -1, -1, -1},
171                {  5,  2, -1,  6},
172        },{
173                {  0,  0, -1,  1},
174                {  3,  1, -1,  4},
175        }});
176
177        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
178                new Point(4, 4), new Point(3, 3)).migrateWorkspace();
179
180        // Items in the second column of the first screen should get placed on the 3rd
181        // row of the second screen
182        verifyWorkspace(new long[][][] {{
183                {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
184                {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
185                {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
186        }, {
187                {ids[1][0][0], ids[1][0][1], ids[1][0][3]},
188                {ids[1][1][0], ids[1][1][1], ids[1][1][3]},
189                {ids[0][0][2], ids[0][1][2], -1},
190        }});
191    }
192
193    public void testWorkspace_items_not_merged_in_next_screen() throws Exception {
194        // First screen has 2 items that need to be moved, but second screen has only one
195        // empty space after migration (top-left corner)
196        long[][][] ids = createGrid(new int[][][]{{
197                {  0,  0,  0,  1},
198                {  3,  1,  0,  4},
199                { -1, -1, -1, -1},
200                {  5,  2, -1,  6},
201        },{
202                { -1,  0, -1,  1},
203                {  3,  1, -1,  4},
204                { -1, -1, -1, -1},
205                {  5,  2, -1,  6},
206        }});
207
208        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
209                new Point(4, 4), new Point(3, 3)).migrateWorkspace();
210
211        // Items in the second column of the first screen should get placed on a new screen.
212        verifyWorkspace(new long[][][] {{
213                {ids[0][0][0], ids[0][0][1], ids[0][0][3]},
214                {ids[0][1][0], ids[0][1][1], ids[0][1][3]},
215                {ids[0][3][0], ids[0][3][1], ids[0][3][3]},
216        }, {
217                {          -1, ids[1][0][1], ids[1][0][3]},
218                {ids[1][1][0], ids[1][1][1], ids[1][1][3]},
219                {ids[1][3][0], ids[1][3][1], ids[1][3][3]},
220        }, {
221                {ids[0][0][2], ids[0][1][2], -1},
222        }});
223    }
224
225    public void testWorkspace_first_row_blocked() throws Exception {
226        // The first screen has one item on the 4th column which needs moving, as the first row
227        // will be kept empty.
228        long[][][] ids = createGrid(new int[][][]{{
229                { -1, -1, -1, -1},
230                {  3,  1,  7,  0},
231                {  8,  7,  7, -1},
232                {  5,  2,  7, -1},
233        }}, 0);
234
235        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
236                new Point(4, 4), new Point(3, 4)).migrateWorkspace();
237
238        // Items in the second column of the first screen should get placed on a new screen.
239        verifyWorkspace(new long[][][] {{
240                {          -1,           -1,           -1},
241                {ids[0][1][0], ids[0][1][1], ids[0][1][2]},
242                {ids[0][2][0], ids[0][2][1], ids[0][2][2]},
243                {ids[0][3][0], ids[0][3][1], ids[0][3][2]},
244        }, {
245                {ids[0][1][3]},
246        }});
247    }
248
249    public void testWorkspace_items_moved_to_empty_first_row() throws Exception {
250        // Items will get moved to the next screen to keep the first screen empty.
251        long[][][] ids = createGrid(new int[][][]{{
252                { -1, -1, -1, -1},
253                {  0,  1,  0,  0},
254                {  8,  7,  7, -1},
255                {  5,  6,  7, -1},
256        }}, 0);
257
258        new GridSizeMigrationTask(getMockContext(), mIdp, mValidPackages,
259                new Point(4, 4), new Point(3, 3)).migrateWorkspace();
260
261        // Items in the second column of the first screen should get placed on a new screen.
262        verifyWorkspace(new long[][][] {{
263                {          -1,           -1,           -1},
264                {ids[0][2][0], ids[0][2][1], ids[0][2][2]},
265                {ids[0][3][0], ids[0][3][1], ids[0][3][2]},
266        }, {
267                {ids[0][1][1], ids[0][1][0], ids[0][1][2]},
268                {ids[0][1][3]},
269        }});
270    }
271
272    private long[][][] createGrid(int[][][] typeArray) throws Exception {
273        return createGrid(typeArray, 1);
274    }
275
276    /**
277     * Initializes the DB with dummy elements to represent the provided grid structure.
278     * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for
279     *                  type definitions. The first dimension represents the screens and the next
280     *                  two represent the workspace grid.
281     * @return the same grid representation where each entry is the corresponding item id.
282     */
283    private long[][][] createGrid(int[][][] typeArray, long startScreen) throws Exception {
284        LauncherSettings.Settings.call(getMockContentResolver(),
285                LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
286        long[][][] ids = new long[typeArray.length][][];
287
288        for (int i = 0; i < typeArray.length; i++) {
289            // Add screen to DB
290            long screenId = startScreen + i;
291
292            // Keep the screen id counter up to date
293            LauncherSettings.Settings.call(getMockContentResolver(),
294                    LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
295
296            ContentValues v = new ContentValues();
297            v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
298            v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
299            getMockContentResolver().insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v);
300
301            ids[i] = new long[typeArray[i].length][];
302            for (int y = 0; y < typeArray[i].length; y++) {
303                ids[i][y] = new long[typeArray[i][y].length];
304                for (int x = 0; x < typeArray[i][y].length; x++) {
305                    if (typeArray[i][y][x] < 0) {
306                        // Empty cell
307                        ids[i][y][x] = -1;
308                    } else {
309                        ids[i][y][x] = addItem(typeArray[i][y][x], screenId, DESKTOP, x, y);
310                    }
311                }
312            }
313        }
314        return ids;
315    }
316
317    /**
318     * Verifies that the workspace items are arranged in the provided order.
319     * @param ids A 3d array where the first dimension represents the screen, and the rest two
320     *            represent the workspace grid.
321     */
322    private void verifyWorkspace(long[][][] ids) {
323        ArrayList<Long> allScreens = LauncherModel.loadWorkspaceScreensDb(getMockContext());
324        assertEquals(ids.length, allScreens.size());
325        int total = 0;
326
327        for (int i = 0; i < ids.length; i++) {
328            long screenId = allScreens.get(i);
329            for (int y = 0; y < ids[i].length; y++) {
330                for (int x = 0; x < ids[i][y].length; x++) {
331                    long id = ids[i][y][x];
332
333                    Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
334                            new String[]{LauncherSettings.Favorites._ID},
335                            "container=-100 and screen=" + screenId +
336                                    " and cellX=" + x + " and cellY=" + y, null, null, null);
337                    if (id == -1) {
338                        assertEquals(0, c.getCount());
339                    } else {
340                        assertEquals(1, c.getCount());
341                        c.moveToNext();
342                        assertEquals(String.format("Failed to verify item at %d %d, %d", i, y, x),
343                                id, c.getLong(0));
344                        total++;
345                    }
346                    c.close();
347                }
348            }
349        }
350
351        // Verify that not other entry exist in the DB.
352        Cursor c = getMockContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
353                new String[]{LauncherSettings.Favorites._ID},
354                "container=-100", null, null, null);
355        assertEquals(total, c.getCount());
356        c.close();
357    }
358
359    /**
360     * Adds a dummy item in the DB.
361     * @param type {@link #APPLICATION} or {@link #SHORTCUT} or >= 2 for
362     *             folder (where the type represents the number of items in the folder).
363     */
364    private long addItem(int type, long screen, long container, int x, int y) throws Exception {
365        long id = LauncherSettings.Settings.call(getMockContentResolver(),
366                LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
367                .getLong(LauncherSettings.Settings.EXTRA_VALUE);
368
369        ContentValues values = new ContentValues();
370        values.put(LauncherSettings.Favorites._ID, id);
371        values.put(LauncherSettings.Favorites.CONTAINER, container);
372        values.put(LauncherSettings.Favorites.SCREEN, screen);
373        values.put(LauncherSettings.Favorites.CELLX, x);
374        values.put(LauncherSettings.Favorites.CELLY, y);
375        values.put(LauncherSettings.Favorites.SPANX, 1);
376        values.put(LauncherSettings.Favorites.SPANY, 1);
377
378        if (type == APPLICATION || type == SHORTCUT) {
379            values.put(LauncherSettings.Favorites.ITEM_TYPE, type);
380            values.put(LauncherSettings.Favorites.INTENT, VALID_INTENT);
381        } else {
382            values.put(LauncherSettings.Favorites.ITEM_TYPE,
383                    LauncherSettings.Favorites.ITEM_TYPE_FOLDER);
384            // Add folder items.
385            for (int i = 0; i < type; i++) {
386                addItem(APPLICATION, 0, id, 0, 0);
387            }
388        }
389
390        getMockContentResolver().insert(LauncherSettings.Favorites.CONTENT_URI, values);
391        return id;
392    }
393
394    public void testMultiStepMigration_small_to_large() throws Exception {
395        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier();
396        verifier.migrate(new Point(3, 3), new Point(5, 5));
397        verifier.assertCompleted();
398    }
399
400    public void testMultiStepMigration_large_to_small() throws Exception {
401        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier(
402                5, 5, 4, 4,
403                4, 4, 3, 4
404        );
405        verifier.migrate(new Point(5, 5), new Point(3, 4));
406        verifier.assertCompleted();
407    }
408
409    public void testMultiStepMigration_zig_zag() throws Exception {
410        MultiStepMigrationTaskVerifier verifier = new MultiStepMigrationTaskVerifier(
411                5, 7, 4, 7,
412                4, 7, 3, 7
413        );
414        verifier.migrate(new Point(5, 5), new Point(3, 7));
415        verifier.assertCompleted();
416    }
417
418    private static class MultiStepMigrationTaskVerifier extends MultiStepMigrationTask {
419
420        private final LinkedList<Point> mPoints;
421
422        public MultiStepMigrationTaskVerifier(int... points) {
423            super(null, null);
424
425            mPoints = new LinkedList<>();
426            for (int i = 0; i < points.length; i += 2) {
427                mPoints.add(new Point(points[i], points[i + 1]));
428            }
429        }
430
431        @Override
432        protected boolean runStepTask(Point sourceSize, Point nextSize) throws Exception {
433            assertEquals(sourceSize, mPoints.poll());
434            assertEquals(nextSize, mPoints.poll());
435            return false;
436        }
437
438        public void assertCompleted() {
439            assertTrue(mPoints.isEmpty());
440        }
441    }
442}
443