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 for (int i = 0; 161 i < FLINGS_FOR_DISMISS_LIMIT 162 && !verifyActiveContainer().hasObject(clearAllSelector); 163 ++i) { 164 flingForwardImpl(); 165 } 166 167 final Runnable clickClearAll = () -> mLauncher.clickLauncherObject( 168 mLauncher.waitForObjectInContainer(verifyActiveContainer(), 169 clearAllSelector)); 170 if (mLauncher.is3PLauncher()) { 171 mLauncher.executeAndWaitForLauncherStop( 172 clickClearAll, 173 "clicking 'Clear All'"); 174 } else { 175 mLauncher.runToState( 176 clickClearAll, 177 NORMAL_STATE_ORDINAL, 178 "clicking 'Clear All'"); 179 } 180 181 mLauncher.waitUntilLauncherObjectGone(clearAllSelector); 182 } 183 } 184 185 /** 186 * Touch to the right of current task. This should dismiss overview and go back to Workspace. 187 */ touchOutsideFirstTask()188 public Workspace touchOutsideFirstTask() { 189 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 190 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 191 "touching outside the focused task")) { 192 193 if (getTaskCount() < 2) { 194 throw new IllegalStateException( 195 "Need to have at least 2 tasks"); 196 } 197 198 OverviewTask currentTask = flingToFirstTask(); 199 200 mLauncher.runToState( 201 () -> mLauncher.touchOutsideContainer(currentTask.getUiObject(), 202 /* tapRight= */ true, 203 /* halfwayToEdge= */ false), 204 NORMAL_STATE_ORDINAL, 205 "touching outside of first task"); 206 207 return new Workspace(mLauncher); 208 } 209 } 210 211 /** 212 * Touch between two tasks 213 */ touchBetweenTasks()214 public void touchBetweenTasks() { 215 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 216 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 217 "touching outside the focused task")) { 218 if (getTaskCount() < 2) { 219 throw new IllegalStateException( 220 "Need to have at least 2 tasks"); 221 } 222 223 OverviewTask currentTask = flingToFirstTask(); 224 225 mLauncher.touchOutsideContainer(currentTask.getUiObject(), 226 /* tapRight= */ false, 227 /* halfwayToEdge= */ false); 228 } 229 } 230 231 /** 232 * Touch either on the right or the left corner of the screen, 1 pixel from the bottom and 233 * from the sides. 234 */ touchTaskbarBottomCorner(boolean tapRight)235 public void touchTaskbarBottomCorner(boolean tapRight) { 236 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 237 Taskbar taskbar = new Taskbar(mLauncher); 238 if (mLauncher.isTransientTaskbar()) { 239 mLauncher.runToState( 240 () -> taskbar.touchBottomCorner(tapRight), 241 NORMAL_STATE_ORDINAL, 242 "touching taskbar"); 243 // Tapping outside Transient Taskbar returns to Workspace, wait for that state. 244 new Workspace(mLauncher); 245 } else { 246 taskbar.touchBottomCorner(tapRight); 247 // Should stay in Overview. 248 verifyActiveContainer(); 249 verifyActionsViewVisibility(); 250 } 251 } 252 } 253 254 /** 255 * Scrolls the current task via flinging forward until it is off screen. 256 * 257 * If only one task is present, it is only partially scrolled off screen and will still be 258 * the current task. 259 */ scrollCurrentTaskOffScreen()260 public void scrollCurrentTaskOffScreen() { 261 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 262 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 263 "want to scroll current task off screen in overview")) { 264 verifyActiveContainer(); 265 266 OverviewTask task = getCurrentTask(); 267 mLauncher.assertNotNull("current task is null", task); 268 mLauncher.scrollLeftByDistance(verifyActiveContainer(), 269 mLauncher.getRealDisplaySize().x - task.getUiObject().getVisibleBounds().left 270 + mLauncher.getOverviewPageSpacing()); 271 272 try (LauncherInstrumentation.Closable c2 = 273 mLauncher.addContextLayer("scrolled task off screen")) { 274 verifyActiveContainer(); 275 verifyActionsViewVisibility(); 276 277 if (getTaskCount() > 1) { 278 if (mLauncher.isTablet()) { 279 mLauncher.assertTrue("current task is not grid height", 280 getCurrentTask().getVisibleHeight() == mLauncher 281 .getOverviewGridTaskSize().height()); 282 } 283 mLauncher.assertTrue("Current task not scrolled off screen", 284 !getCurrentTask().equals(task)); 285 } 286 } 287 } 288 } 289 290 /** 291 * Gets the current task in the carousel, or fails if the carousel is empty. 292 * 293 * @return the task in the middle of the visible tasks list. 294 */ 295 @NonNull getCurrentTask()296 public OverviewTask getCurrentTask() { 297 UiObject2 currentTask = getCurrentTaskUnchecked(); 298 mLauncher.assertNotNull("Unable to find a task", currentTask); 299 return new OverviewTask(mLauncher, currentTask, this); 300 } 301 302 @Nullable getCurrentTaskUnchecked()303 private UiObject2 getCurrentTaskUnchecked() { 304 final List<UiObject2> taskViews = getTasks(); 305 if (taskViews.isEmpty()) { 306 return null; 307 } 308 309 // The widest, and most top-right task should be the current task 310 return Collections.max(taskViews, 311 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width()) 312 .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x) 313 .thenComparing(Comparator.comparing( 314 (UiObject2 t) -> t.getVisibleCenter().y).reversed())); 315 } 316 317 /** 318 * Returns an overview task that contains the specified test activity in its thumbnails. 319 * 320 * @param activityIndex index of TestActivity to match against 321 */ 322 @NonNull getTestActivityTask(int activityIndex)323 public OverviewTask getTestActivityTask(int activityIndex) { 324 return getTestActivityTask(Collections.singleton(activityIndex)); 325 } 326 327 /** 328 * Returns an overview task that contains all the specified test activities in its thumbnails. 329 * 330 * @param activityNumbers collection of indices of TestActivity to match against 331 */ 332 @NonNull getTestActivityTask(Collection<Integer> activityNumbers)333 public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) { 334 final List<UiObject2> taskViews = getTasks(); 335 mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size()); 336 337 Optional<UiObject2> task = taskViews.stream().filter( 338 taskView -> activityNumbers.stream().allMatch(activityNumber -> 339 // TODO(b/239452415): Use equals instead of descEndsWith 340 taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber)) 341 )).findFirst(); 342 343 mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers 344 + " from the task list", task.isPresent()); 345 346 return new OverviewTask(mLauncher, task.get(), this); 347 } 348 349 /** 350 * Returns a list of all tasks fully visible in the tablet grid overview. 351 */ 352 @NonNull getCurrentTasksForTablet()353 public List<OverviewTask> getCurrentTasksForTablet() { 354 final List<UiObject2> taskViews = getTasks(); 355 mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size()); 356 357 final int gridTaskWidth = mLauncher.getOverviewGridTaskSize().width(); 358 359 return taskViews.stream().filter(t -> t.getVisibleBounds().width() == gridTaskWidth).map( 360 t -> new OverviewTask(mLauncher, t, this)).collect(Collectors.toList()); 361 } 362 363 @NonNull getTasks()364 private List<UiObject2> getTasks() { 365 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 366 "want to get overview tasks")) { 367 verifyActiveContainer(); 368 return mLauncher.getDevice().findObjects(TASK_SELECTOR); 369 } 370 } 371 getTaskCount()372 int getTaskCount() { 373 return getTasks().size(); 374 } 375 376 /** 377 * Returns whether Overview has tasks. 378 */ hasTasks()379 public boolean hasTasks() { 380 return getTasks().size() > 0; 381 } 382 383 /** 384 * Gets Overview Actions. 385 * 386 * @return The Overview Actions 387 */ 388 @NonNull getOverviewActions()389 public OverviewActions getOverviewActions() { 390 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 391 "want to get overview actions")) { 392 verifyActiveContainer(); 393 UiObject2 overviewActions = mLauncher.waitForOverviewObject("action_buttons"); 394 return new OverviewActions(overviewActions, mLauncher); 395 } 396 } 397 398 /** 399 * Returns if clear all button is visible. 400 */ isClearAllVisible()401 public boolean isClearAllVisible() { 402 return verifyActiveContainer().hasObject( 403 mLauncher.getOverviewObjectSelector("clear_all")); 404 } 405 406 /** 407 * Returns the taskbar if it's a tablet, or {@code null} otherwise. 408 */ 409 @Nullable getTaskbar()410 public Taskbar getTaskbar() { 411 if (!mLauncher.isTablet()) { 412 return null; 413 } 414 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 415 "want to get the taskbar")) { 416 mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID); 417 418 return new Taskbar(mLauncher); 419 } 420 } 421 isActionsViewVisible()422 protected boolean isActionsViewVisible() { 423 if (!hasTasks() || isClearAllVisible()) { 424 testLogD(TAG, "Not expecting an actions bar: no tasks/'Clear all' is visible"); 425 return false; 426 } 427 boolean isTablet = mLauncher.isTablet(); 428 if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) { 429 testLogD(TAG, "Not expecting an actions bar: device is tablet with grid-only Overview"); 430 return false; 431 } 432 OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask(); 433 if (task == null) { 434 testLogD(TAG, "Not expecting an actions bar: no current task"); 435 return false; 436 } 437 // In tablets, if focused task is not in center, overview actions aren't visible. 438 if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) { 439 testLogD(TAG, 440 "Not expecting an actions bar: device is tablet and task is not centered"); 441 return false; 442 } 443 if (task.isGrouped() && (!mLauncher.isAppPairsEnabled() || !isTablet)) { 444 testLogD(TAG, "Not expecting an actions bar: device is phone and task is split"); 445 // Overview actions aren't visible for split screen tasks, except for save app pair 446 // button on tablets. 447 return false; 448 } 449 testLogD(TAG, "Expecting an actions bar"); 450 return true; 451 } 452 453 /** 454 * Presses the esc key to dismiss Overview. 455 */ dismissByEscKey()456 public Workspace dismissByEscKey() { 457 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 458 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP); 459 mLauncher.runToState( 460 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE), 461 NORMAL_STATE_ORDINAL, "pressing esc key"); 462 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 463 "pressed esc key")) { 464 return mLauncher.getWorkspace(); 465 } 466 } 467 } 468 469 /** 470 * Presses the enter key to launch the focused task 471 * <p> 472 * If no task is focused, this will fail. 473 */ launchFocusedTaskByEnterKey(@onNull String expectedPackageName)474 public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) { 475 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 476 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP); 477 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT); 478 479 mLauncher.executeAndWaitForLauncherStop( 480 () -> mLauncher.assertTrue( 481 "Failed to press enter", 482 mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)), 483 "pressing enter"); 484 mLauncher.assertAppLaunched(expectedPackageName); 485 486 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 487 "pressed enter")) { 488 return new LaunchedAppState(mLauncher); 489 } 490 } 491 } 492 verifyActionsViewVisibility()493 private void verifyActionsViewVisibility() { 494 // If no running tasks, no need to verify actions view visibility. 495 if (getTasks().isEmpty()) { 496 return; 497 } 498 499 boolean isTablet = mLauncher.isTablet(); 500 OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask(); 501 502 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 503 "want to assert overview actions view visibility=" 504 + isActionsViewVisible() 505 + ", focused task is " 506 + (task == null ? "null" : (task.isGrouped() ? "split" : "not split")) 507 )) { 508 509 if (isActionsViewVisible()) { 510 if (task.isGrouped()) { 511 mLauncher.waitForOverviewObject("action_save_app_pair"); 512 } else { 513 mLauncher.waitForOverviewObject("action_buttons"); 514 } 515 } else { 516 mLauncher.waitUntilOverviewObjectGone("action_buttons"); 517 mLauncher.waitUntilOverviewObjectGone("action_save_app_pair"); 518 } 519 } 520 } 521 522 /** 523 * Returns Overview focused task if it exists. 524 * 525 * @throws IllegalStateException if not run on a tablet device. 526 */ getFocusedTaskForTablet()527 OverviewTask getFocusedTaskForTablet() { 528 if (!mLauncher.isTablet()) { 529 throw new IllegalStateException("Must be run on tablet device."); 530 } 531 final List<UiObject2> taskViews = getTasks(); 532 if (taskViews.isEmpty()) { 533 return null; 534 } 535 Rect focusTaskSize = mLauncher.getOverviewTaskSize(); 536 int focusedTaskHeight = focusTaskSize.height(); 537 for (UiObject2 task : taskViews) { 538 OverviewTask overviewTask = new OverviewTask(mLauncher, task, this); 539 // Desktop tasks can't be focused tasks, but are the same size. 540 if (overviewTask.isDesktop()) { 541 continue; 542 } 543 if (overviewTask.getVisibleHeight() == focusedTaskHeight) { 544 return overviewTask; 545 } 546 } 547 return null; 548 } 549 isLiveTile(UiObject2 task)550 protected boolean isLiveTile(UiObject2 task) { 551 // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence 552 // compare only hashCode as a workaround. 553 return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode(); 554 } 555 } 556