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