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 com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL; 20 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; 21 import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL; 22 23 import static junit.framework.TestCase.assertTrue; 24 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.os.SystemClock; 28 import android.view.KeyEvent; 29 import android.view.MotionEvent; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.test.uiautomator.By; 34 import androidx.test.uiautomator.Direction; 35 import androidx.test.uiautomator.UiObject2; 36 37 import com.android.launcher3.testing.TestProtocol; 38 39 import java.util.List; 40 import java.util.function.Supplier; 41 import java.util.regex.Pattern; 42 import java.util.stream.Collectors; 43 44 /** 45 * Operations on the workspace screen. 46 */ 47 public final class Workspace extends Home { 48 private static final int FLING_STEPS = 10; 49 private static final int DEFAULT_DRAG_STEPS = 10; 50 private static final String DROP_BAR_RES_ID = "drop_target_bar"; 51 private static final String DELETE_TARGET_TEXT_ID = "delete_target_text"; 52 53 static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile( 54 "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W" 55 + ".*?metaState=META_CTRL_ON"); 56 static final Pattern EVENT_CTRL_W_UP = Pattern.compile( 57 "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W" 58 + ".*?metaState=META_CTRL_ON"); 59 private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick"); 60 61 private final UiObject2 mHotseat; 62 Workspace(LauncherInstrumentation launcher)63 Workspace(LauncherInstrumentation launcher) { 64 super(launcher); 65 mHotseat = launcher.waitForLauncherObject("hotseat"); 66 } 67 68 /** 69 * Swipes up to All Apps. 70 * 71 * @return the All Apps object. 72 */ 73 @NonNull switchToAllApps()74 public AllApps switchToAllApps() { 75 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 76 LauncherInstrumentation.Closable c = 77 mLauncher.addContextLayer("want to switch from workspace to all apps")) { 78 verifyActiveContainer(); 79 final int deviceHeight = mLauncher.getDevice().getDisplayHeight(); 80 final int bottomGestureMargin = mLauncher.getBottomGestureSize(); 81 final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius()); 82 final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1; 83 final int swipeHeight = mLauncher.getTestInfo( 84 TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT). 85 getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 86 LauncherInstrumentation.log( 87 "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY 88 + ", swipeHeight = " + swipeHeight + ", slop = " 89 + mLauncher.getTouchSlop()); 90 91 mLauncher.swipeToState( 92 windowCornerRadius, 93 startY, 94 windowCornerRadius, 95 startY - swipeHeight - mLauncher.getTouchSlop(), 96 12, 97 ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE); 98 99 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 100 "swiped to all apps")) { 101 return new AllApps(mLauncher); 102 } 103 } 104 } 105 106 /** 107 * Returns an icon for the app, if currently visible. 108 * 109 * @param appName name of the app 110 * @return app icon, if found, null otherwise. 111 */ 112 @Nullable tryGetWorkspaceAppIcon(String appName)113 public AppIcon tryGetWorkspaceAppIcon(String appName) { 114 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 115 "want to get a workspace icon")) { 116 final UiObject2 workspace = verifyActiveContainer(); 117 final UiObject2 icon = workspace.findObject( 118 AppIcon.getAppIconSelector(appName, mLauncher)); 119 return icon != null ? new AppIcon(mLauncher, icon) : null; 120 } 121 } 122 123 124 /** 125 * Returns an icon for the app; fails if the icon doesn't exist. 126 * 127 * @param appName name of the app 128 * @return app icon. 129 */ 130 @NonNull getWorkspaceAppIcon(String appName)131 public AppIcon getWorkspaceAppIcon(String appName) { 132 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 133 "want to get a workspace icon")) { 134 return new AppIcon(mLauncher, 135 mLauncher.waitForObjectInContainer( 136 verifyActiveContainer(), 137 AppIcon.getAppIconSelector(appName, mLauncher))); 138 } 139 } 140 141 /** 142 * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the 143 * second screen. 144 */ ensureWorkspaceIsScrollable()145 public void ensureWorkspaceIsScrollable() { 146 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 147 final UiObject2 workspace = verifyActiveContainer(); 148 if (!isWorkspaceScrollable(workspace)) { 149 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 150 "dragging icon to a second page of workspace to make it scrollable")) { 151 dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen()); 152 verifyActiveContainer(); 153 } 154 } 155 assertTrue("Home screen workspace didn't become scrollable", 156 isWorkspaceScrollable(workspace)); 157 } 158 } 159 160 /** 161 * Returns the number of pages that are visible on the screen simultaneously. 162 */ pagesPerScreen()163 public int pagesPerScreen() { 164 return mLauncher.isTwoPanels() ? 2 : 1; 165 } 166 167 /** 168 * Drags an icon to the (currentPage + pageDelta) page if the page already exists. 169 * If the target page doesn't exist, the icon will be put onto an existing page that is the 170 * closest to the target page. 171 * 172 * @param appIcon - icon to drag. 173 * @param pageDelta - how many pages should the icon be dragged from the current page. 174 * It can be a negative value. 175 */ dragIcon(AppIcon appIcon, int pageDelta)176 public void dragIcon(AppIcon appIcon, int pageDelta) { 177 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 178 final UiObject2 workspace = verifyActiveContainer(); 179 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 180 "dragging icon to page with delta: " + pageDelta)) { 181 dragIcon(workspace, appIcon, pageDelta); 182 verifyActiveContainer(); 183 } 184 } 185 } 186 dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta)187 private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) { 188 int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen(); 189 int targetX = (pageWidth / 2) + pageWidth * pageDelta; 190 dragIconToWorkspace( 191 mLauncher, 192 appIcon, 193 new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()), 194 "popup_container", 195 false, 196 false, 197 () -> mLauncher.expectEvent( 198 TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)); 199 verifyActiveContainer(); 200 } 201 isWorkspaceScrollable(UiObject2 workspace)202 private boolean isWorkspaceScrollable(UiObject2 workspace) { 203 return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1); 204 } 205 206 @NonNull getHotseatAppIcon(String appName)207 public AppIcon getHotseatAppIcon(String appName) { 208 return new AppIcon(mLauncher, mLauncher.waitForObjectInContainer( 209 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher))); 210 } 211 getStartDragThreshold(LauncherInstrumentation launcher)212 private static int getStartDragThreshold(LauncherInstrumentation launcher) { 213 return launcher.getTestInfo(TestProtocol.REQUEST_START_DRAG_THRESHOLD).getInt( 214 TestProtocol.TEST_INFO_RESPONSE_FIELD); 215 } 216 217 /* 218 * Get the center point of the delete icon in the drop target bar. 219 */ getDeleteDropPoint()220 private Point getDeleteDropPoint() { 221 return mLauncher.waitForObjectInContainer( 222 mLauncher.waitForLauncherObject(DROP_BAR_RES_ID), 223 DELETE_TARGET_TEXT_ID).getVisibleCenter(); 224 } 225 226 /** 227 * Delete the appIcon from the workspace. 228 * 229 * @param appIcon to be deleted. 230 * @return validated workspace after the existing appIcon being deleted. 231 */ deleteAppIcon(AppIcon appIcon)232 public Workspace deleteAppIcon(AppIcon appIcon) { 233 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 234 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 235 "removing app icon from workspace")) { 236 dragIconToWorkspace( 237 mLauncher, appIcon, 238 () -> getDeleteDropPoint(), 239 true, /* decelerating */ 240 appIcon.getLongPressIndicator(), 241 () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), 242 null); 243 244 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 245 "dragged the app to the drop bar")) { 246 return new Workspace(mLauncher); 247 } 248 } 249 } 250 251 /** 252 * Finds folder icons in the current workspace. 253 * 254 * @return a list of folder icons. 255 */ getFolderIcons()256 List<FolderIcon> getFolderIcons() { 257 final UiObject2 workspace = verifyActiveContainer(); 258 return mLauncher.getObjectsInContainer(workspace, "folder_icon_name").stream().map( 259 o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList()); 260 } 261 262 /** 263 * Drag an icon up with a short distance that makes workspace go to spring loaded state. 264 * 265 * @return the position after dragging. 266 */ dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime, UiObject2 icon, String longPressIndicator, Runnable expectLongClickEvents)267 private static Point dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime, 268 UiObject2 icon, 269 String longPressIndicator, Runnable expectLongClickEvents) { 270 final Point iconCenter = icon.getVisibleCenter(); 271 final Point dragStartCenter = new Point(iconCenter.x, 272 iconCenter.y - getStartDragThreshold(launcher)); 273 274 launcher.runToState(() -> { 275 launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, 276 iconCenter, LauncherInstrumentation.GestureScope.INSIDE); 277 LauncherInstrumentation.log("dragIconToSpringLoaded: sent down"); 278 expectLongClickEvents.run(); 279 launcher.waitForLauncherObject(longPressIndicator); 280 LauncherInstrumentation.log("dragIconToSpringLoaded: indicator"); 281 launcher.movePointer(iconCenter, dragStartCenter, DEFAULT_DRAG_STEPS, false, 282 downTime, true, LauncherInstrumentation.GestureScope.INSIDE); 283 }, SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start"); 284 return dragStartCenter; 285 } 286 dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, @Nullable Runnable expectedEvents)287 private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, 288 @Nullable Runnable expectedEvents) { 289 launcher.runToState( 290 () -> launcher.sendPointer( 291 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest, 292 LauncherInstrumentation.GestureScope.INSIDE), 293 NORMAL_STATE_ORDINAL, 294 "sending UP event"); 295 if (expectedEvents != null) { 296 expectedEvents.run(); 297 } 298 LauncherInstrumentation.log("dropIcon: end"); 299 launcher.waitUntilLauncherObjectGone("drop_target_bar"); 300 } 301 dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents)302 static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, 303 Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, 304 Runnable expectLongClickEvents) { 305 Runnable expectDropEvents = null; 306 if (startsActivity || isWidgetShortcut) { 307 expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, 308 LauncherInstrumentation.EVENT_START); 309 } 310 dragIconToWorkspace(launcher, launchable, () -> dest, false, longPressIndicator, 311 expectLongClickEvents, expectDropEvents); 312 } 313 314 /** 315 * Drag icon in workspace to else where. 316 * This function expects the launchable is inside the workspace and there is no drop event. 317 */ dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> destSupplier, String longPressIndicator)318 static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, 319 Supplier<Point> destSupplier, String longPressIndicator) { 320 dragIconToWorkspace(launcher, launchable, destSupplier, false, longPressIndicator, 321 () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), null); 322 } 323 dragIconToWorkspace( LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> dest, boolean isDecelerating, String longPressIndicator, Runnable expectLongClickEvents, @Nullable Runnable expectDropEvents)324 static void dragIconToWorkspace( 325 LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> dest, 326 boolean isDecelerating, String longPressIndicator, Runnable expectLongClickEvents, 327 @Nullable Runnable expectDropEvents) { 328 try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer( 329 "want to drag icon to workspace")) { 330 final long downTime = SystemClock.uptimeMillis(); 331 Point dragStart = dragIconToSpringLoaded(launcher, downTime, 332 launchable.getObject(), longPressIndicator, expectLongClickEvents); 333 Point targetDest = dest.get(); 334 int displayX = launcher.getRealDisplaySize().x; 335 336 // Since the destination can be on another page, we need to drag to the edge first 337 // until we reach the target page 338 while (targetDest.x > displayX || targetDest.x < 0) { 339 int edgeX = targetDest.x > 0 ? displayX : 0; 340 Point screenEdge = new Point(edgeX, targetDest.y); 341 launcher.movePointer(dragStart, screenEdge, DEFAULT_DRAG_STEPS, isDecelerating, 342 downTime, true, LauncherInstrumentation.GestureScope.INSIDE); 343 launcher.waitForIdle(); // Wait for the page change to happen 344 targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1); 345 dragStart = screenEdge; 346 } 347 348 // targetDest.x is now between 0 and displayX so we found the target page, 349 // we just have to put move the icon to the destination and drop it 350 launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating, 351 downTime, true, LauncherInstrumentation.GestureScope.INSIDE); 352 dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents); 353 } 354 } 355 356 /** 357 * Flings to get to screens on the right. Waits for scrolling and a possible overscroll 358 * recoil to complete. 359 */ flingForward()360 public void flingForward() { 361 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 362 final UiObject2 workspace = verifyActiveContainer(); 363 mLauncher.scroll(workspace, Direction.RIGHT, 364 new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0), 365 FLING_STEPS, false); 366 verifyActiveContainer(); 367 } 368 } 369 370 /** 371 * Flings to get to screens on the left. Waits for scrolling and a possible overscroll 372 * recoil to complete. 373 */ flingBackward()374 public void flingBackward() { 375 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 376 final UiObject2 workspace = verifyActiveContainer(); 377 mLauncher.scroll(workspace, Direction.LEFT, 378 new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0), 379 FLING_STEPS, false); 380 verifyActiveContainer(); 381 } 382 } 383 384 /** 385 * Opens widgets container by pressing Ctrl+W. 386 * 387 * @return the widgets container. 388 */ 389 @NonNull openAllWidgets()390 public Widgets openAllWidgets() { 391 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 392 verifyActiveContainer(); 393 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_DOWN); 394 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP); 395 mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON); 396 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) { 397 return new Widgets(mLauncher); 398 } 399 } 400 } 401 402 @Override getSwipeHeightRequestName()403 protected String getSwipeHeightRequestName() { 404 return TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT; 405 } 406 407 @Override getSwipeStartY()408 protected int getSwipeStartY() { 409 return mLauncher.getRealDisplaySize().y - 1; 410 } 411 412 @Nullable tryGetWidget(String label, long timeout)413 public Widget tryGetWidget(String label, long timeout) { 414 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 415 "getting widget " + label + " on workspace with timeout " + timeout)) { 416 final UiObject2 widget = mLauncher.tryWaitForLauncherObject( 417 By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label), 418 timeout); 419 return widget != null ? new Widget(mLauncher, widget) : null; 420 } 421 } 422 423 @Nullable tryGetPendingWidget(long timeout)424 public Widget tryGetPendingWidget(long timeout) { 425 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 426 "getting pending widget on workspace with timeout " + timeout)) { 427 final UiObject2 widget = mLauncher.tryWaitForLauncherObject( 428 By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout); 429 return widget != null ? new Widget(mLauncher, widget) : null; 430 } 431 } 432 }