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_ESCAPE; 20 21 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID; 22 import static com.android.launcher3.tapl.LauncherInstrumentation.log; 23 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT; 24 import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName; 25 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 26 import static com.android.launcher3.testing.shared.TestProtocol.testLogD; 27 28 import android.graphics.Rect; 29 import android.view.KeyEvent; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.test.uiautomator.By; 34 import androidx.test.uiautomator.BySelector; 35 import androidx.test.uiautomator.Direction; 36 import androidx.test.uiautomator.UiObject2; 37 38 import com.android.launcher3.testing.shared.TestProtocol; 39 40 import java.util.Collection; 41 import java.util.Collections; 42 import java.util.Comparator; 43 import java.util.List; 44 import java.util.Optional; 45 import java.util.regex.Pattern; 46 import java.util.stream.Collectors; 47 48 /** 49 * Common overview panel for both Launcher and fallback recents 50 */ 51 public class BaseOverview extends LauncherInstrumentation.VisibleContainer { 52 private static final String TAG = "BaseOverview"; 53 protected static final BySelector TASK_SELECTOR = By.res(Pattern.compile( 54 getOverviewPackageName() 55 + ":id/(task_view_single|task_view_grouped|task_view_desktop)")); 56 private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile( 57 "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0"); 58 private static final Pattern EVENT_ENTER_DOWN = Pattern.compile( 59 "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ENTER"); 60 private static final Pattern EVENT_ENTER_UP = Pattern.compile( 61 "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ENTER"); 62 63 private static final int FLINGS_FOR_DISMISS_LIMIT = 40; 64 65 private final @Nullable UiObject2 mLiveTileTask; 66 67 BaseOverview(LauncherInstrumentation launcher)68 BaseOverview(LauncherInstrumentation launcher) { 69 this(launcher, /*launchedFromApp=*/false); 70 } 71 BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp)72 BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp) { 73 super(launcher); 74 verifyActiveContainer(); 75 verifyActionsViewVisibility(); 76 if (launchedFromApp) { 77 mLiveTileTask = getCurrentTaskUnchecked(); 78 } else { 79 mLiveTileTask = null; 80 } 81 } 82 83 @Override getContainerType()84 protected LauncherInstrumentation.ContainerType getContainerType() { 85 return LauncherInstrumentation.ContainerType.FALLBACK_OVERVIEW; 86 } 87 88 /** 89 * Flings forward (left) and waits the fling's end. 90 */ flingForward()91 public void flingForward() { 92 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 93 flingForwardImpl(); 94 } 95 } 96 flingForwardImpl()97 private void flingForwardImpl() { 98 try (LauncherInstrumentation.Closable c = 99 mLauncher.addContextLayer("want to fling forward in overview")) { 100 log("Overview.flingForward before fling"); 101 final UiObject2 overview = verifyActiveContainer(); 102 final int leftMargin = 103 mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth(); 104 mLauncher.scroll(overview, Direction.LEFT, new Rect(leftMargin + 1, 0, 0, 0), 20, 105 false); 106 try (LauncherInstrumentation.Closable c2 = 107 mLauncher.addContextLayer("flung forwards")) { 108 verifyActiveContainer(); 109 verifyActionsViewVisibility(); 110 } 111 } 112 } 113 114 /** 115 * Flings backward (right) and waits the fling's end. 116 */ flingBackward()117 public void flingBackward() { 118 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 119 flingBackwardImpl(); 120 } 121 } 122 flingBackwardImpl()123 private void flingBackwardImpl() { 124 try (LauncherInstrumentation.Closable c = 125 mLauncher.addContextLayer("want to fling backward in overview")) { 126 log("Overview.flingBackward before fling"); 127 final UiObject2 overview = verifyActiveContainer(); 128 final int rightMargin = 129 mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth(); 130 mLauncher.scroll( 131 overview, Direction.RIGHT, new Rect(0, 0, rightMargin + 1, 0), 20, false); 132 try (LauncherInstrumentation.Closable c2 = 133 mLauncher.addContextLayer("flung backwards")) { 134 verifyActiveContainer(); 135 verifyActionsViewVisibility(); 136 } 137 } 138 } 139 flingToFirstTask()140 private OverviewTask flingToFirstTask() { 141 OverviewTask currentTask = getCurrentTask(); 142 143 while (mLauncher.getRealDisplaySize().x - currentTask.getUiObject().getVisibleBounds().right 144 <= mLauncher.getOverviewPageSpacing()) { 145 flingBackwardImpl(); 146 currentTask = getCurrentTask(); 147 } 148 149 return currentTask; 150 } 151 152 /** 153 * Dismissed all tasks by scrolling to Clear-all button and pressing it. 154 */ dismissAllTasks()155 public void dismissAllTasks() { 156 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 157 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 158 "dismissing all tasks")) { 159 final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all"); 160 flingForwardUntilClearAllVisibleImpl(); 161 162 final Runnable clickClearAll = () -> mLauncher.clickLauncherObject( 163 mLauncher.waitForObjectInContainer(verifyActiveContainer(), 164 clearAllSelector)); 165 if (mLauncher.is3PLauncher()) { 166 mLauncher.executeAndWaitForLauncherStop( 167 clickClearAll, 168 "clicking 'Clear All'"); 169 } else { 170 mLauncher.runToState( 171 clickClearAll, 172 NORMAL_STATE_ORDINAL, 173 "clicking 'Clear All'"); 174 } 175 176 mLauncher.waitUntilLauncherObjectGone(clearAllSelector); 177 } 178 } 179 180 /** 181 * Scrolls until Clear-all button is visible. 182 */ flingForwardUntilClearAllVisible()183 public void flingForwardUntilClearAllVisible() { 184 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 185 flingForwardUntilClearAllVisibleImpl(); 186 } 187 } 188 flingForwardUntilClearAllVisibleImpl()189 private void flingForwardUntilClearAllVisibleImpl() { 190 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 191 "flinging forward to clear all")) { 192 final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all"); 193 for (int i = 0; i < FLINGS_FOR_DISMISS_LIMIT && !verifyActiveContainer().hasObject( 194 clearAllSelector); ++i) { 195 flingForwardImpl(); 196 } 197 } 198 } 199 200 /** 201 * Touch to the right of current task. This should dismiss overview and go back to Workspace. 202 */ touchOutsideFirstTask()203 public Workspace touchOutsideFirstTask() { 204 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 205 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 206 "touching outside the focused task")) { 207 208 if (getTaskCount() < 2) { 209 throw new IllegalStateException( 210 "Need to have at least 2 tasks"); 211 } 212 213 OverviewTask currentTask = flingToFirstTask(); 214 215 mLauncher.runToState( 216 () -> mLauncher.touchOutsideContainer(currentTask.getUiObject(), 217 /* tapRight= */ true, 218 /* halfwayToEdge= */ false), 219 NORMAL_STATE_ORDINAL, 220 "touching outside of first task"); 221 222 return new Workspace(mLauncher); 223 } 224 } 225 226 /** 227 * Touch between two tasks 228 */ touchBetweenTasks()229 public void touchBetweenTasks() { 230 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 231 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 232 "touching outside the focused task")) { 233 if (getTaskCount() < 2) { 234 throw new IllegalStateException( 235 "Need to have at least 2 tasks"); 236 } 237 238 OverviewTask currentTask = flingToFirstTask(); 239 240 mLauncher.touchOutsideContainer(currentTask.getUiObject(), 241 /* tapRight= */ false, 242 /* halfwayToEdge= */ false); 243 } 244 } 245 246 /** 247 * Touch either on the right or the left corner of the screen, 1 pixel from the bottom and 248 * from the sides. 249 */ touchTaskbarBottomCorner(boolean tapRight)250 public void touchTaskbarBottomCorner(boolean tapRight) { 251 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 252 Taskbar taskbar = new Taskbar(mLauncher); 253 if (mLauncher.isTransientTaskbar()) { 254 mLauncher.runToState( 255 () -> taskbar.touchBottomCorner(tapRight), 256 NORMAL_STATE_ORDINAL, 257 "touching taskbar"); 258 // Tapping outside Transient Taskbar returns to Workspace, wait for that state. 259 new Workspace(mLauncher); 260 } else { 261 taskbar.touchBottomCorner(tapRight); 262 // Should stay in Overview. 263 verifyActiveContainer(); 264 verifyActionsViewVisibility(); 265 } 266 } 267 } 268 269 /** 270 * Scrolls the current task via flinging forward until it is off screen. 271 * 272 * If only one task is present, it is only partially scrolled off screen and will still be 273 * the current task. 274 */ scrollCurrentTaskOffScreen()275 public void scrollCurrentTaskOffScreen() { 276 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 277 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 278 "want to scroll current task off screen in overview")) { 279 verifyActiveContainer(); 280 281 OverviewTask task = getCurrentTask(); 282 mLauncher.assertNotNull("current task is null", task); 283 mLauncher.scrollLeftByDistance(verifyActiveContainer(), 284 mLauncher.getRealDisplaySize().x - task.getUiObject().getVisibleBounds().left 285 + mLauncher.getOverviewPageSpacing()); 286 287 try (LauncherInstrumentation.Closable c2 = 288 mLauncher.addContextLayer("scrolled task off screen")) { 289 verifyActiveContainer(); 290 verifyActionsViewVisibility(); 291 292 if (getTaskCount() > 1) { 293 if (mLauncher.isTablet()) { 294 mLauncher.assertTrue("current task is not grid height", 295 getCurrentTask().getVisibleHeight() == mLauncher 296 .getOverviewGridTaskSize().height()); 297 } 298 mLauncher.assertTrue("Current task not scrolled off screen", 299 !getCurrentTask().equals(task)); 300 } 301 } 302 } 303 } 304 305 /** 306 * Gets the current task in the carousel, or fails if the carousel is empty. 307 * 308 * @return the task in the middle of the visible tasks list. 309 */ 310 @NonNull getCurrentTask()311 public OverviewTask getCurrentTask() { 312 UiObject2 currentTask = getCurrentTaskUnchecked(); 313 mLauncher.assertNotNull("Unable to find a task", currentTask); 314 return new OverviewTask(mLauncher, currentTask, this); 315 } 316 317 @Nullable getCurrentTaskUnchecked()318 private UiObject2 getCurrentTaskUnchecked() { 319 final List<UiObject2> taskViews = getTasks(); 320 if (taskViews.isEmpty()) { 321 return null; 322 } 323 324 // The widest, and most top-right task should be the current task 325 return Collections.max(taskViews, 326 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width()) 327 .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x) 328 .thenComparing(Comparator.comparing( 329 (UiObject2 t) -> t.getVisibleCenter().y).reversed())); 330 } 331 332 /** 333 * Returns an overview task that contains the specified test activity in its thumbnails. 334 * 335 * @param activityIndex index of TestActivity to match against 336 */ 337 @NonNull getTestActivityTask(int activityIndex)338 public OverviewTask getTestActivityTask(int activityIndex) { 339 return getTestActivityTask(Collections.singleton(activityIndex)); 340 } 341 342 /** 343 * Returns an overview task that contains all the specified test activities in its thumbnails. 344 * 345 * @param activityNumbers collection of indices of TestActivity to match against 346 */ 347 @NonNull getTestActivityTask(Collection<Integer> activityNumbers)348 public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) { 349 final List<UiObject2> taskViews = getTasks(); 350 mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size()); 351 352 Optional<UiObject2> task = taskViews.stream().filter( 353 taskView -> activityNumbers.stream().allMatch(activityNumber -> 354 // TODO(b/239452415): Use equals instead of descEndsWith 355 taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber)) 356 )).findFirst(); 357 358 mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers 359 + " from the task list", task.isPresent()); 360 361 return new OverviewTask(mLauncher, task.get(), this); 362 } 363 364 /** 365 * Returns a list of all tasks fully visible in the tablet grid overview. 366 */ 367 @NonNull getCurrentTasksForTablet()368 public List<OverviewTask> getCurrentTasksForTablet() { 369 final List<UiObject2> taskViews = getTasks(); 370 mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size()); 371 372 final int gridTaskWidth = mLauncher.getOverviewGridTaskSize().width(); 373 374 return taskViews.stream().filter(t -> t.getVisibleBounds().width() == gridTaskWidth).map( 375 t -> new OverviewTask(mLauncher, t, this)).collect(Collectors.toList()); 376 } 377 378 @NonNull getTasks()379 private List<UiObject2> getTasks() { 380 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 381 "want to get overview tasks")) { 382 verifyActiveContainer(); 383 return mLauncher.getDevice().findObjects(TASK_SELECTOR); 384 } 385 } 386 getTaskCount()387 int getTaskCount() { 388 return getTasks().size(); 389 } 390 391 /** 392 * Returns whether Overview has tasks. 393 */ hasTasks()394 public boolean hasTasks() { 395 return getTasks().size() > 0; 396 } 397 398 /** 399 * Gets Overview Actions. 400 * 401 * @return The Overview Actions 402 */ 403 @NonNull getOverviewActions()404 public OverviewActions getOverviewActions() { 405 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 406 "want to get overview actions")) { 407 verifyActiveContainer(); 408 UiObject2 overviewActions = mLauncher.waitForOverviewObject("action_buttons"); 409 return new OverviewActions(overviewActions, mLauncher); 410 } 411 } 412 413 /** 414 * Returns if clear all button is visible. 415 */ isClearAllVisible()416 public boolean isClearAllVisible() { 417 return verifyActiveContainer().hasObject( 418 mLauncher.getOverviewObjectSelector("clear_all")); 419 } 420 421 /** 422 * Returns the taskbar if it's a tablet, or {@code null} otherwise. 423 */ 424 @Nullable getTaskbar()425 public Taskbar getTaskbar() { 426 if (!mLauncher.isTablet()) { 427 return null; 428 } 429 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 430 "want to get the taskbar")) { 431 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID); 432 433 return new Taskbar(mLauncher); 434 } 435 } 436 isActionsViewVisible()437 protected boolean isActionsViewVisible() { 438 if (!hasTasks() || isClearAllVisible()) { 439 testLogD(TAG, "Not expecting an actions bar: no tasks/'Clear all' is visible"); 440 return false; 441 } 442 boolean isTablet = mLauncher.isTablet(); 443 if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) { 444 testLogD(TAG, "Not expecting an actions bar: device is tablet with grid-only Overview"); 445 return false; 446 } 447 OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask(); 448 if (task == null) { 449 testLogD(TAG, "Not expecting an actions bar: no current task"); 450 return false; 451 } 452 // In tablets, if focused task is not in center, overview actions aren't visible. 453 if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) { 454 testLogD(TAG, 455 "Not expecting an actions bar: device is tablet and task is not centered"); 456 return false; 457 } 458 if (task.isGrouped() && !isTablet) { 459 testLogD(TAG, "Not expecting an actions bar: device is phone and task is split"); 460 // Overview actions aren't visible for split screen tasks, except for save app pair 461 // button on tablets. 462 return false; 463 } 464 testLogD(TAG, "Expecting an actions bar"); 465 return true; 466 } 467 468 /** 469 * Presses the esc key to dismiss Overview. 470 */ dismissByEscKey()471 public Workspace dismissByEscKey() { 472 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 473 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP); 474 mLauncher.runToState( 475 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE), 476 NORMAL_STATE_ORDINAL, "pressing esc key"); 477 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 478 "pressed esc key")) { 479 return mLauncher.getWorkspace(); 480 } 481 } 482 } 483 484 /** 485 * Presses the enter key to launch the focused task 486 * <p> 487 * If no task is focused, this will fail. 488 */ launchFocusedTaskByEnterKey(@onNull String expectedPackageName)489 public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) { 490 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 491 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP); 492 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT); 493 494 mLauncher.executeAndWaitForLauncherStop( 495 () -> mLauncher.assertTrue( 496 "Failed to press enter", 497 mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)), 498 "pressing enter"); 499 mLauncher.assertAppLaunched(expectedPackageName); 500 501 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 502 "pressed enter")) { 503 return new LaunchedAppState(mLauncher); 504 } 505 } 506 } 507 verifyActionsViewVisibility()508 private void verifyActionsViewVisibility() { 509 // If no running tasks, no need to verify actions view visibility. 510 if (getTasks().isEmpty()) { 511 return; 512 } 513 514 boolean isTablet = mLauncher.isTablet(); 515 OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask(); 516 517 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 518 "want to assert overview actions view visibility=" 519 + isActionsViewVisible() 520 + ", focused task is " 521 + (task == null ? "null" : (task.isGrouped() ? "split" : "not split")) 522 )) { 523 524 if (isActionsViewVisible()) { 525 if (task.isGrouped()) { 526 mLauncher.waitForOverviewObject("action_save_app_pair"); 527 } else { 528 mLauncher.waitForOverviewObject("action_buttons"); 529 } 530 } else { 531 mLauncher.waitUntilOverviewObjectGone("action_buttons"); 532 mLauncher.waitUntilOverviewObjectGone("action_save_app_pair"); 533 } 534 } 535 } 536 537 /** 538 * Returns Overview focused task if it exists. 539 * 540 * @throws IllegalStateException if not run on a tablet device. 541 */ getFocusedTaskForTablet()542 OverviewTask getFocusedTaskForTablet() { 543 if (!mLauncher.isTablet()) { 544 throw new IllegalStateException("Must be run on tablet device."); 545 } 546 final List<UiObject2> taskViews = getTasks(); 547 if (taskViews.isEmpty()) { 548 return null; 549 } 550 Rect focusTaskSize = mLauncher.getOverviewTaskSize(); 551 int focusedTaskHeight = focusTaskSize.height(); 552 for (UiObject2 task : taskViews) { 553 OverviewTask overviewTask = new OverviewTask(mLauncher, task, this); 554 // Desktop tasks can't be focused tasks, but are the same size. 555 if (overviewTask.isDesktop()) { 556 continue; 557 } 558 if (overviewTask.getVisibleHeight() == focusedTaskHeight) { 559 return overviewTask; 560 } 561 } 562 return null; 563 } 564 isLiveTile(UiObject2 task)565 protected boolean isLiveTile(UiObject2 task) { 566 // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence 567 // compare only hashCode as a workaround. 568 return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode(); 569 } 570 } 571