1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.tapl; 18 19 import static android.view.KeyEvent.KEYCODE_META_RIGHT; 20 import static android.view.KeyEvent.KEYCODE_RECENT_APPS; 21 import static android.view.KeyEvent.KEYCODE_TAB; 22 import static android.view.KeyEvent.META_META_ON; 23 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED; 24 25 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL; 26 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 27 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL; 28 29 import static junit.framework.TestCase.assertNotNull; 30 import static junit.framework.TestCase.assertTrue; 31 32 import android.content.Intent; 33 import android.graphics.Point; 34 import android.graphics.Rect; 35 import android.os.SystemClock; 36 import android.view.KeyEvent; 37 import android.view.MotionEvent; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.test.uiautomator.By; 42 import androidx.test.uiautomator.BySelector; 43 import androidx.test.uiautomator.Direction; 44 import androidx.test.uiautomator.UiDevice; 45 import androidx.test.uiautomator.UiObject2; 46 import androidx.test.uiautomator.Until; 47 48 import com.android.launcher3.testing.shared.TestProtocol; 49 50 import java.util.List; 51 import java.util.Map; 52 import java.util.function.Supplier; 53 import java.util.regex.Pattern; 54 import java.util.stream.Collectors; 55 56 /** 57 * Operations on the workspace screen. 58 */ 59 public final class Workspace extends Home { 60 private static final int FLING_STEPS = 10; 61 private static final int DEFAULT_DRAG_STEPS = 20; 62 private static final String DROP_BAR_RES_ID = "drop_target_bar"; 63 private static final String DELETE_TARGET_TEXT_ID = "delete_target_text"; 64 private static final String UNINSTALL_TARGET_TEXT_ID = "uninstall_target_text"; 65 static final Pattern EVENT_CTRL_W_UP = Pattern.compile( 66 "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W" 67 + ".*?metaState=META_CTRL_ON"); 68 static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick"); 69 public static final int MAX_WORKSPACE_DRAG_TRIES = 100; 70 71 private final UiObject2 mHotseat; 72 Workspace(LauncherInstrumentation launcher)73 Workspace(LauncherInstrumentation launcher) { 74 super(launcher); 75 mHotseat = launcher.waitForLauncherObject("hotseat"); 76 } 77 78 /** 79 * Swipes up to All Apps. 80 * 81 * @return the All Apps object. 82 */ 83 @NonNull switchToAllApps()84 public HomeAllApps switchToAllApps() { 85 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 86 LauncherInstrumentation.Closable c = 87 mLauncher.addContextLayer("want to switch from workspace to all apps")) { 88 verifyActiveContainer(); 89 final int deviceHeight = mLauncher.getDevice().getDisplayHeight(); 90 final int bottomGestureMargin = mLauncher.getBottomGestureSize(); 91 final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius()); 92 final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1; 93 final int swipeHeight = mLauncher.getTestInfo( 94 TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT) 95 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 96 LauncherInstrumentation.log( 97 "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY 98 + ", swipeHeight = " + swipeHeight + ", slop = " 99 + mLauncher.getTouchSlop()); 100 101 mLauncher.swipeToState( 102 windowCornerRadius, 103 startY, 104 windowCornerRadius, 105 startY - swipeHeight - mLauncher.getTouchSlop(), 106 12, 107 ALL_APPS_STATE_ORDINAL, 108 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); 109 110 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 111 "swiped to all apps")) { 112 return new HomeAllApps(mLauncher); 113 } 114 } 115 } 116 117 /** Opens the Launcher all apps page with the meta keyboard shortcut. */ openAllAppsFromKeyboardShortcut()118 public HomeAllApps openAllAppsFromKeyboardShortcut() { 119 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 120 LauncherInstrumentation.Closable c = 121 mLauncher.addContextLayer("want to open all apps search")) { 122 verifyActiveContainer(); 123 mLauncher.runToState( 124 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT), 125 ALL_APPS_STATE_ORDINAL, 126 "pressing keyboard shortcut"); 127 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 128 "pressed meta key")) { 129 return new HomeAllApps(mLauncher); 130 } 131 } 132 } 133 134 /** Opens the Launcher Overview page with the action+tab keyboard shortcut. */ openOverviewFromActionPlusTabKeyboardShortcut()135 public Overview openOverviewFromActionPlusTabKeyboardShortcut() { 136 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 137 LauncherInstrumentation.Closable c = 138 mLauncher.addContextLayer("want to open overview")) { 139 verifyActiveContainer(); 140 mLauncher.runToState( 141 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_TAB, META_META_ON), 142 OVERVIEW_STATE_ORDINAL, 143 "pressing keyboard shortcut"); 144 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 145 "pressed meta+tab key")) { 146 return new Overview(mLauncher); 147 } 148 } 149 } 150 151 /** Opens the Launcher Overview page with the Recents keyboard shortcut. */ openOverviewFromRecentsKeyboardShortcut()152 public Overview openOverviewFromRecentsKeyboardShortcut() { 153 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 154 LauncherInstrumentation.Closable c = 155 mLauncher.addContextLayer("want to open overview")) { 156 verifyActiveContainer(); 157 mLauncher.runToState( 158 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_RECENT_APPS), 159 OVERVIEW_STATE_ORDINAL, 160 "pressing keyboard shortcut"); 161 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 162 "pressed recents apps key")) { 163 return new Overview(mLauncher); 164 } 165 } 166 } 167 168 /** 169 * Returns the home qsb. 170 * 171 * The qsb must already be visible when calling this method. 172 */ 173 @NonNull getQsb()174 public Qsb getQsb() { 175 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 176 "want to get the home qsb")) { 177 return new HomeQsb(mLauncher, mHotseat); 178 } 179 } 180 181 /** 182 * Returns an icon for the app, if currently visible. 183 * 184 * @param appName name of the app 185 * @return app icon, if found, null otherwise. 186 */ 187 @Nullable tryGetWorkspaceAppIcon(String appName)188 public HomeAppIcon tryGetWorkspaceAppIcon(String appName) { 189 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 190 "want to get a workspace icon")) { 191 final UiObject2 workspace = verifyActiveContainer(); 192 final UiObject2 icon = workspace.findObject( 193 AppIcon.getAppIconSelector(appName, mLauncher)); 194 return icon != null ? new WorkspaceAppIcon(mLauncher, icon) : null; 195 } 196 } 197 198 /** 199 * Waits for an app icon to be gone (e.g. after uninstall). Fails if it remains. 200 * 201 * @param errorMessage error message thrown then the icon doesn't disappear. 202 * @param appName app that should be gone. 203 */ verifyWorkspaceAppIconIsGone(String errorMessage, String appName)204 public void verifyWorkspaceAppIconIsGone(String errorMessage, String appName) { 205 final UiObject2 workspace = verifyActiveContainer(); 206 assertTrue(errorMessage, 207 workspace.wait( 208 Until.gone(AppIcon.getAppIconSelector(appName, mLauncher)), 209 LauncherInstrumentation.WAIT_TIME_MS)); 210 } 211 212 213 /** 214 * Returns an icon for the app; fails if the icon doesn't exist. 215 * 216 * @param appName name of the app 217 * @return app icon. 218 */ 219 @NonNull getWorkspaceAppIcon(String appName)220 public HomeAppIcon getWorkspaceAppIcon(String appName) { 221 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 222 "want to get a workspace icon")) { 223 return new WorkspaceAppIcon(mLauncher, 224 mLauncher.waitForObjectInContainer( 225 verifyActiveContainer(), 226 AppIcon.getAppIconSelector(appName, mLauncher))); 227 } 228 } 229 230 /** 231 * Ensures that workspace is scrollable. If it's not, drags a chrome app icon from hotseat 232 * to the second screen. 233 */ ensureWorkspaceIsScrollable()234 public void ensureWorkspaceIsScrollable() { 235 ensureWorkspaceIsScrollable("Chrome"); 236 } 237 238 /** 239 * Ensures that workspace is scrollable. If it's not, drags an icon of a given app name from 240 * hotseat to the second screen. 241 */ ensureWorkspaceIsScrollable(String appName)242 public void ensureWorkspaceIsScrollable(String appName) { 243 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 244 final UiObject2 workspace = verifyActiveContainer(); 245 if (!isWorkspaceScrollable(workspace)) { 246 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 247 "dragging icon to a second page of workspace to make it scrollable")) { 248 dragIcon(workspace, getHotseatAppIcon(appName), pagesPerScreen()); 249 verifyActiveContainer(); 250 } 251 } 252 assertTrue("Home screen workspace didn't become scrollable", 253 isWorkspaceScrollable(workspace)); 254 } 255 } 256 257 /** Returns the number of pages. */ getPageCount()258 public int getPageCount() { 259 final UiObject2 workspace = verifyActiveContainer(); 260 return workspace.getChildCount(); 261 } 262 263 /** 264 * Returns the number of pages that are visible on the screen simultaneously. 265 */ pagesPerScreen()266 public int pagesPerScreen() { 267 return mLauncher.isTwoPanels() ? 2 : 1; 268 } 269 270 /** 271 * Drags an icon to the (currentPage + pageDelta) page. 272 * If the target page doesn't exist yet, a new page will be created. 273 * In case the target page can't be created (e.g. existing pages are 0, 1, current: 0, 274 * pageDelta: 3, the latest page that can be created is 2) the icon will be dragged onto the 275 * page that can be created and is closest to the target page. 276 * 277 * @param homeAppIcon - icon to drag. 278 * @param pageDelta - how many pages should the icon be dragged from the current page. 279 * It can be a negative value. currentPage + pageDelta should be greater 280 * than or equal to 0. 281 */ dragIcon(HomeAppIcon homeAppIcon, int pageDelta)282 public void dragIcon(HomeAppIcon homeAppIcon, int pageDelta) { 283 if (mHotseat.getVisibleBounds().height() > mHotseat.getVisibleBounds().width()) { 284 throw new UnsupportedOperationException( 285 "dragIcon does NOT support dragging when the hotseat is on the side."); 286 } 287 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 288 final UiObject2 workspace = verifyActiveContainer(); 289 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 290 "dragging icon to page with delta: " + pageDelta)) { 291 dragIcon(workspace, homeAppIcon, pageDelta); 292 verifyActiveContainer(); 293 } 294 } 295 } 296 dragIcon(UiObject2 workspace, HomeAppIcon homeAppIcon, int pageDelta)297 private void dragIcon(UiObject2 workspace, HomeAppIcon homeAppIcon, int pageDelta) { 298 int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen(); 299 int targetX = (pageWidth / 2) + pageWidth * pageDelta; 300 int targetY = mLauncher.getVisibleBounds(workspace).centerY(); 301 dragIconToWorkspace( 302 mLauncher, 303 homeAppIcon, 304 () -> new Point(targetX, targetY), 305 false, 306 false, 307 () -> mLauncher.expectEvent( 308 TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)); 309 verifyActiveContainer(); 310 } 311 isWorkspaceScrollable(UiObject2 workspace)312 private boolean isWorkspaceScrollable(UiObject2 workspace) { 313 return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1); 314 } 315 316 @NonNull getHotseatAppIcon(String appName)317 public HomeAppIcon getHotseatAppIcon(String appName) { 318 return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer( 319 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher))); 320 } 321 322 /** 323 * Returns an icon for the given cell; fails if the icon doesn't exist. 324 * 325 * @param cellInd zero based index number of the hotseat cells. 326 * @return app icon. 327 */ 328 @NonNull getHotseatAppIcon(int cellInd)329 public HomeAppIcon getHotseatAppIcon(int cellInd) { 330 List<UiObject2> icons = mHotseat.findObjects(AppIcon.getAnyAppIconSelector()); 331 final Point center = getHotseatCellCenter(mLauncher, cellInd); 332 return icons.stream() 333 .filter(icon -> icon.getVisibleBounds().contains(center.x, center.y)) 334 .findFirst() 335 .map(icon -> new WorkspaceAppIcon(mLauncher, icon)) 336 .orElseThrow(() -> 337 new AssertionError("Unable to get a hotseat icon on " + cellInd)); 338 } 339 340 /** 341 * @return map of text -> center of the view. In case of icons with the same name, the one with 342 * lower x coordinate is selected. 343 */ getAllWorkspaceIconsPositions()344 public Map<String, Point> getAllWorkspaceIconsPositions() { 345 final UiObject2 workspace = verifyActiveContainer(); 346 List<UiObject2> workspaceIcons = 347 mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector()); 348 return getIconPositionMap(workspaceIcons); 349 } 350 351 /** 352 * @return point where icon is found for given the app name, 353 * point is visible center of the icon. 354 */ 355 @NonNull getWorkspaceIconPosition(String appName)356 public Point getWorkspaceIconPosition(String appName) { 357 final UiObject2 workspace = verifyActiveContainer(); 358 359 UiObject2 workspaceIcon = 360 mLauncher.waitForObjectInContainer(workspace, 361 AppIcon.getAppIconSelector(appName, mLauncher)); 362 return workspaceIcon.getVisibleCenter(); 363 } 364 getIconPositionMap(List<UiObject2> icons)365 private Map<String, Point> getIconPositionMap(List<UiObject2> icons) { 366 return icons.stream() 367 .collect( 368 Collectors.toMap( 369 /* keyMapper= */ uiObject21 -> { 370 return uiObject21.getText(); 371 }, 372 /* valueMapper= */ uiObject2 -> { 373 return uiObject2.getVisibleCenter(); 374 }, 375 /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2)); 376 } 377 378 /* 379 * Get the center point of the delete/uninstall icon in the drop target bar. 380 */ 381 private static Point getDropPointFromDropTargetBar( 382 LauncherInstrumentation launcher, String targetId) { 383 return launcher.waitForObjectInContainer( 384 launcher.waitForLauncherObject(DROP_BAR_RES_ID), 385 targetId).getVisibleCenter(); 386 } 387 388 /** 389 * Drag the appIcon from the workspace and cancel by dragging icon to corner of screen where no 390 * drop point exists. 391 * 392 * @param homeAppIcon to be dragged. 393 */ 394 @NonNull 395 public Workspace dragAndCancelAppIcon(HomeAppIcon homeAppIcon) { 396 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 397 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 398 "dragging app icon across workspace")) { 399 dragIconToWorkspace( 400 mLauncher, 401 homeAppIcon, 402 () -> new Point(0, 0), mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)403 () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), 404 null, 405 /* startsActivity = */ false); 406 try(LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "dragged the app across workspace"))407 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 408 "dragged the app across workspace")) { 409 return new Workspace(mLauncher); 410 } 411 } 412 } 413 414 /** 415 * Delete the appIcon from the workspace. 416 * 417 * @param homeAppIcon to be deleted. 418 * @return validated workspace after the existing appIcon being deleted. 419 */ 420 public Workspace deleteAppIcon(HomeAppIcon homeAppIcon) { 421 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 422 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 423 "removing app icon from workspace")) { 424 dragIconToWorkspace( 425 mLauncher, 426 homeAppIcon, 427 () -> getDropPointFromDropTargetBar(mLauncher, DELETE_TARGET_TEXT_ID), 428 () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), 429 /* expectDropEvents= */ null, 430 /* startsActivity = */ false); 431 432 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 433 "dragged the app to the drop bar")) { 434 return new Workspace(mLauncher); 435 } 436 } 437 } 438 439 /** 440 * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar. 441 * 442 * @param launcher the root TAPL instrumentation object of {@link 443 * LauncherInstrumentation} type. 444 * @param homeAppIcon to be uninstalled. 445 * @param launcher the root TAPL instrumentation object of {@link 446 * LauncherInstrumentation} type. 447 * @param homeAppIcon to be uninstalled. 448 * @param expectLongClickEvents the runnable to be executed to verify expected longclick event. 449 * @return validated workspace after the existing appIcon being uninstalled. 450 */ 451 static Workspace uninstallAppIcon(LauncherInstrumentation launcher, HomeAppIcon homeAppIcon, 452 Runnable expectLongClickEvents) { 453 try (LauncherInstrumentation.Closable c = launcher.addContextLayer( 454 "uninstalling app icon")) { 455 456 final String appNameToUninstall = homeAppIcon.getAppName(); 457 dragIconToWorkspace( 458 launcher, 459 homeAppIcon, 460 () -> getDropPointFromDropTargetBar(launcher, UNINSTALL_TARGET_TEXT_ID), 461 expectLongClickEvents, 462 /* expectDropEvents= */null, 463 /* startsActivity = */ false); 464 465 launcher.waitUntilLauncherObjectGone(DROP_BAR_RES_ID); 466 467 final BySelector installerAlert = By.text(Pattern.compile( 468 "Do you want to uninstall this app\\?", 469 Pattern.DOTALL | Pattern.MULTILINE)); 470 final UiDevice device = launcher.getDevice(); 471 assertTrue("uninstall alert is not shown", device.wait( 472 Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS)); 473 final UiObject2 ok = device.findObject(By.text("OK")); 474 assertNotNull("OK button is not shown", ok); 475 launcher.clickObject(ok); 476 assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait( 477 Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS)); 478 479 try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer( 480 "uninstalled app by dragging to the drop bar")) { 481 final Workspace newWorkspace = new Workspace(launcher); 482 launcher.waitUntilLauncherObjectGone( 483 AppIcon.getAppIconSelector(appNameToUninstall)); 484 return newWorkspace; 485 } 486 } 487 } 488 489 /** 490 * Get cell layout's grids size. The return point's x and y values are the cell counts in X and 491 * Y directions respectively, not the values in pixels. 492 */ 493 public Point getIconGridDimensions() { 494 int[] countXY = mLauncher.getTestInfo( 495 TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE).getIntArray( 496 TestProtocol.TEST_INFO_RESPONSE_FIELD); 497 return new Point(countXY[0], countXY[1]); 498 } 499 500 static Point getCellCenter(LauncherInstrumentation launcher, int cellX, int cellY) { 501 return getCellCenter(launcher, cellX, cellY, 1, 1); 502 } 503 504 static Point getCellCenter(LauncherInstrumentation launcher, int cellX, int cellY, int spanX, 505 int spanY) { 506 return launcher.getTestInfo( 507 new Intent(TestProtocol.REQUEST_WORKSPACE_CELL_CENTER) 508 .putExtra(TestProtocol.TEST_INFO_PARAM_CELL_SPAN, 509 new Rect(cellX, cellY, cellX + spanX, cellY + spanY))) 510 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD); 511 } 512 513 static Point getHotseatCellCenter(LauncherInstrumentation launcher, int cellIndex) { 514 return launcher.getTestInfo( 515 new Intent(TestProtocol.REQUEST_HOTSEAT_CELL_CENTER) 516 .putExtra(TestProtocol.TEST_INFO_PARAM_INDEX, cellIndex)) 517 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD); 518 } 519 520 /** Returns the number of rows and columns in the workspace */ 521 public Point getRowsAndCols() { 522 return mLauncher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_COLUMNS_ROWS).getParcelable( 523 TestProtocol.TEST_INFO_RESPONSE_FIELD); 524 } 525 526 /** Returns the index of the current page */ 527 public int getCurrentPage() { 528 return getCurrentPage(mLauncher); 529 } 530 531 /** Returns the index of the current page */ 532 private static int getCurrentPage(LauncherInstrumentation launcher) { 533 return launcher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX).getInt( 534 TestProtocol.TEST_INFO_RESPONSE_FIELD); 535 } 536 537 /** 538 * Finds folder icons in the current workspace. 539 * 540 * @return a list of folder icons. 541 */ 542 List<FolderIcon> getFolderIcons() { 543 final UiObject2 workspace = verifyActiveContainer(); 544 return mLauncher.getObjectsInContainer(workspace, "folder_icon_name").stream().map( 545 o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList()); 546 } 547 548 private static void sendUp(LauncherInstrumentation launcher, Point dest, 549 long downTime) { 550 launcher.sendPointer( 551 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest, 552 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); 553 } 554 555 private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, 556 @Nullable Runnable expectedEvents, boolean startsActivity) { 557 if (startsActivity) { 558 launcher.executeAndWaitForLauncherStop( 559 () -> sendUp(launcher, dest, downTime), 560 "sending UP event"); 561 } else { 562 launcher.runToState( 563 () -> sendUp(launcher, dest, downTime), 564 NORMAL_STATE_ORDINAL, 565 "sending UP event"); 566 } 567 if (expectedEvents != null) { 568 expectedEvents.run(); 569 } 570 LauncherInstrumentation.log("dropIcon: end"); 571 launcher.waitUntilLauncherObjectGone("drop_target_bar"); 572 } 573 574 static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, 575 Supplier<Point> dest, boolean startsActivity, boolean isWidgetShortcut, 576 Runnable expectLongClickEvents) { 577 Runnable expectDropEvents = null; 578 if (startsActivity || isWidgetShortcut) { 579 expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, 580 LauncherInstrumentation.EVENT_START); 581 } 582 dragIconToWorkspace( 583 launcher, launchable, dest, expectLongClickEvents, expectDropEvents, 584 startsActivity); 585 } 586 587 static void dragIconToWorkspaceCellPosition(LauncherInstrumentation launcher, 588 Launchable launchable, int cellX, int cellY, int spanX, int spanY, 589 boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents) { 590 Runnable expectDropEvents = null; 591 if (startsActivity || isWidgetShortcut) { 592 expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, 593 LauncherInstrumentation.EVENT_START); 594 } 595 dragIconToWorkspaceCellPosition( 596 launcher, launchable, cellX, cellY, spanX, spanY, true, expectLongClickEvents, 597 expectDropEvents); 598 } 599 600 /** 601 * Drag icon in workspace to else where and drop it immediately. 602 * (There is no slow down time before drop event) 603 * This function expects the launchable is inside the workspace and there is no drop event. 604 */ 605 static void dragIconToWorkspace( 606 LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> destSupplier, 607 boolean isDraggingToFolder) { 608 dragIconToWorkspace( 609 launcher, 610 launchable, 611 destSupplier, 612 /* isDecelerating= */ !isDraggingToFolder, 613 () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), 614 /* expectDropEvents= */ null, 615 /* startsActivity = */ false, 616 isDraggingToFolder); 617 } 618 619 static void dragIconToWorkspace( 620 LauncherInstrumentation launcher, 621 Launchable launchable, 622 Supplier<Point> dest, 623 Runnable expectLongClickEvents, 624 @Nullable Runnable expectDropEvents, 625 boolean startsActivity) { 626 dragIconToWorkspace(launcher, launchable, dest, /* isDecelerating */ true, 627 expectLongClickEvents, expectDropEvents, startsActivity, 628 /* isDraggingToFolder */ false); 629 } 630 631 static void dragIconToWorkspace( 632 LauncherInstrumentation launcher, 633 Launchable launchable, 634 Supplier<Point> dest, 635 boolean isDecelerating, 636 Runnable expectLongClickEvents, 637 @Nullable Runnable expectDropEvents, 638 boolean startsActivity, 639 boolean isDraggingToFolder) { 640 try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( 641 "want to drag icon to workspace")) { 642 final long downTime = SystemClock.uptimeMillis(); 643 Point dragStart = launchable.startDrag( 644 downTime, 645 expectLongClickEvents, 646 /* runToSpringLoadedState= */ true); 647 Point targetDest = dest.get(); 648 int displayX = launcher.getRealDisplaySize().x; 649 650 // Since the destination can be on another page, we need to drag to the edge first 651 // until we reach the target page 652 while (targetDest.x > displayX || targetDest.x < 0) { 653 // Don't drag all the way to the edge to prevent touch events from getting out of 654 //screen bounds. 655 int edgeX = targetDest.x > 0 ? displayX - 1 : 1; 656 Point screenEdge = new Point(edgeX, targetDest.y); 657 Point finalDragStart = dragStart; 658 executeAndWaitForPageScroll(launcher, 659 () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, 660 true, downTime, downTime, true, 661 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER)); 662 targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1); 663 dragStart = screenEdge; 664 } 665 666 // targetDest.x is now between 0 and displayX so we found the target page. 667 // If not a folder, we just have to put move the icon to the destination and drop it. 668 // If it's a folder we want to drag to the folder icon and then drag to the center of 669 // that folder when it opens. 670 if (isDraggingToFolder) { 671 Point finalDragStart = dragStart; 672 Point finalTargetDest = targetDest; 673 Folder folder = executeAndWaitForFolderOpen(launcher, () -> launcher.movePointer( 674 finalDragStart, finalTargetDest, DEFAULT_DRAG_STEPS, isDecelerating, 675 downTime, SystemClock.uptimeMillis(), false, 676 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER)); 677 678 Rect dropBounds = folder.getDropLocationBounds(); 679 dragStart = targetDest; 680 targetDest = new Point(dropBounds.centerX(), dropBounds.centerY()); 681 } 682 683 launcher.movePointer(dragStart, targetDest, 684 DEFAULT_DRAG_STEPS, isDecelerating, downTime, SystemClock.uptimeMillis(), 685 !isDraggingToFolder, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); 686 687 dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, startsActivity); 688 } 689 } 690 691 static void dragIconToWorkspaceCellPosition( 692 LauncherInstrumentation launcher, 693 Launchable launchable, 694 int cellX, int cellY, int spanX, int spanY, 695 boolean isDecelerating, 696 Runnable expectLongClickEvents, 697 @Nullable Runnable expectDropEvents) { 698 try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( 699 "want to drag icon to workspace")) { 700 Point rowsAndCols = launcher.getWorkspace().getRowsAndCols(); 701 int destinationWorkspace = cellX / rowsAndCols.x; 702 cellX = cellX % rowsAndCols.x; 703 704 final long downTime = SystemClock.uptimeMillis(); 705 Point dragStart = launchable.startDrag( 706 downTime, 707 expectLongClickEvents, 708 /* runToSpringLoadedState= */ true); 709 Point targetDest = getCellCenter(launcher, cellX, cellY, spanX, spanY); 710 // Since the destination can be on another page, we need to drag to the edge first 711 // until we reach the target page 712 dragStart = dragToGivenWorkspace(launcher, dragStart, destinationWorkspace, 713 targetDest.y); 714 715 // targetDest.x is now between 0 and displayX so we found the target page, 716 // we just have to put move the icon to the destination and drop it 717 launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating, 718 downTime, SystemClock.uptimeMillis(), false, 719 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); 720 dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, 721 /* startsActivity = */ false); 722 } 723 } 724 725 /** 726 * Given a drag that already started at currentPosition, drag the item to the given destination 727 * index defined by destinationWorkspaceIndex. 728 * 729 * @param launcher 730 * @param currentPosition 731 * @param destinationWorkspaceIndex 732 * @param y 733 * @return the finishing position of the drag. 734 */ 735 private static Point dragToGivenWorkspace(LauncherInstrumentation launcher, 736 Point currentPosition, int destinationWorkspaceIndex, int y) { 737 final long downTime = SystemClock.uptimeMillis(); 738 int displayX = launcher.getRealDisplaySize().x; 739 int currentPage = Workspace.getCurrentPage(launcher); 740 int counter = 0; 741 while (currentPage != destinationWorkspaceIndex) { 742 counter++; 743 if (counter > MAX_WORKSPACE_DRAG_TRIES) { 744 throw new RuntimeException( 745 "Wrong destination workspace index " + destinationWorkspaceIndex 746 + ", desired workspace was never reached"); 747 } 748 // if the destination is greater than current page, set the display edge to be the 749 // right edge. Don't drag all the way to the edge to prevent touch events from 750 // getting out of screen bounds. 751 int displayEdge = destinationWorkspaceIndex > currentPage ? displayX - 1 : 1; 752 Point screenEdge = new Point(displayEdge, y); 753 Point finalDragStart = currentPosition; 754 executeAndWaitForPageScroll(launcher, 755 () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS, 756 true, downTime, downTime, true, 757 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER)); 758 currentPage = Workspace.getCurrentPage(launcher); 759 currentPosition = screenEdge; 760 } 761 return currentPosition; 762 } 763 764 private static void executeAndWaitForPageScroll(LauncherInstrumentation launcher, 765 Runnable command) { 766 launcher.executeAndWaitForEvent(command, 767 event -> event.getEventType() == TYPE_VIEW_SCROLLED, 768 () -> "Page scroll didn't happen", "Scrolling page"); 769 } 770 771 private static Folder executeAndWaitForFolderOpen(LauncherInstrumentation launcher, 772 Runnable command) { 773 launcher.executeAndWaitForEvent(command, 774 event -> TestProtocol.FOLDER_OPENED_MESSAGE.equals( 775 event.getClassName().toString()), 776 () -> "Fail to open folder.", 777 "open folder"); 778 return new Folder(launcher); 779 } 780 781 static void dragIconToHotseat( 782 LauncherInstrumentation launcher, 783 Launchable launchable, 784 Supplier<Point> dest, 785 Runnable expectLongClickEvents, 786 @Nullable Runnable expectDropEvents) { 787 final long downTime = SystemClock.uptimeMillis(); 788 Point dragStart = launchable.startDrag( 789 downTime, 790 expectLongClickEvents, 791 /* runToSpringLoadedState= */ true); 792 Point targetDest = dest.get(); 793 794 launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, true, 795 downTime, SystemClock.uptimeMillis(), false, 796 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER); 797 dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, 798 /* startsActivity = */ false); 799 } 800 801 /** 802 * Flings to get to screens on the right. Waits for scrolling and a possible overscroll 803 * recoil to complete. 804 */ 805 public void flingForward() { 806 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 807 Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer()); 808 mLauncher.pointerScroll( 809 workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.RIGHT); 810 verifyActiveContainer(); 811 } 812 } 813 814 /** 815 * Flings to get to screens on the left. Waits for scrolling and a possible overscroll 816 * recoil to complete. 817 */ 818 public void flingBackward() { 819 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 820 Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer()); 821 mLauncher.pointerScroll( 822 workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.LEFT); 823 verifyActiveContainer(); 824 } 825 } 826 827 /** 828 * Opens widgets container by pressing Ctrl+W. 829 * 830 * @return the widgets container. 831 */ 832 @NonNull 833 public Widgets openAllWidgets() { 834 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 835 verifyActiveContainer(); 836 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP); 837 mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON); 838 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) { 839 return new Widgets(mLauncher); 840 } 841 } 842 } 843 844 @Override 845 protected String getSwipeHeightRequestName() { 846 return mLauncher.isRecentsWindowEnabled() 847 ? super.getSwipeHeightRequestName() 848 : TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT; 849 } 850 851 @Override 852 protected int getSwipeStartY() { 853 return mLauncher.getRealDisplaySize().y - 1; 854 } 855 856 @Nullable 857 public Widget tryGetWidget(String label, long timeout) { 858 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 859 "getting widget " + label + " on workspace with timeout " + timeout)) { 860 final UiObject2 widget = mLauncher.tryWaitForLauncherObject( 861 By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label), 862 timeout); 863 return widget != null ? new Widget(mLauncher, widget) : null; 864 } 865 } 866 867 /** 868 * @param cellX X position of the widget trying to get. 869 * @param cellY Y position of the widget trying to get. 870 * @return returns the Widget in the given position in the Launcher or an Exception if no such 871 * widget is in that position. 872 */ 873 @NonNull 874 public Widget getWidgetAtCell(int cellX, int cellY) { 875 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 876 "getting widget at cell position " + cellX + "," + cellY)) { 877 final List<UiObject2> widgets = mLauncher.waitForObjectsBySelector( 878 By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView")); 879 Point coordinateInScreen = Workspace.getCellCenter(mLauncher, cellX, cellY); 880 for (UiObject2 widget : widgets) { 881 if (widget.getVisibleBounds().contains(coordinateInScreen.x, 882 coordinateInScreen.y)) { 883 return new Widget(mLauncher, widget); 884 } 885 } 886 } 887 mLauncher.fail("Unable to find widget at cell " + cellX + "," + cellY); 888 // This statement is unreachable because mLauncher.fail throws an exception 889 // but is needed for compiling 890 return null; 891 } 892 893 @Nullable 894 public Widget tryGetPendingWidget(long timeout) { 895 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 896 "getting pending widget on workspace with timeout " + timeout)) { 897 final UiObject2 widget = mLauncher.tryWaitForLauncherObject( 898 By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout); 899 return widget != null ? new Widget(mLauncher, widget) : null; 900 } 901 } 902 } 903