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