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