1 package com.android.launcher3.model; 2 3 import android.content.ContentValues; 4 import android.content.Intent; 5 import android.database.Cursor; 6 import android.graphics.Point; 7 import android.test.ProviderTestCase2; 8 import android.test.suitebuilder.annotation.MediumTest; 9 10 import com.android.launcher3.InvariantDeviceProfile; 11 import com.android.launcher3.LauncherModel; 12 import com.android.launcher3.LauncherProvider; 13 import com.android.launcher3.LauncherSettings; 14 import com.android.launcher3.config.FeatureFlags; 15 import com.android.launcher3.model.GridSizeMigrationTask.MultiStepMigrationTask; 16 import com.android.launcher3.util.TestLauncherProvider; 17 18 import java.util.ArrayList; 19 import java.util.HashSet; 20 import java.util.LinkedList; 21 22 /** 23 * Unit tests for {@link GridSizeMigrationTask} 24 */ 25 @MediumTest 26 public 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