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