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