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.tapl.OverviewTask.OverviewTaskContainer.DEFAULT; 20 import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.DESKTOP; 21 import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_BOTTOM_OR_RIGHT; 22 import static com.android.launcher3.tapl.OverviewTask.OverviewTaskContainer.SPLIT_TOP_OR_LEFT; 23 24 import android.graphics.Rect; 25 26 import androidx.annotation.NonNull; 27 import androidx.test.uiautomator.By; 28 import androidx.test.uiautomator.BySelector; 29 import androidx.test.uiautomator.UiObject2; 30 31 import com.android.launcher3.testing.shared.TestProtocol; 32 33 import java.util.List; 34 import java.util.regex.Pattern; 35 import java.util.stream.Collectors; 36 37 /** 38 * A recent task in the overview panel carousel. 39 */ 40 public final class OverviewTask { 41 private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; 42 static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync"); 43 static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents"); 44 static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile( 45 "composeRecentsLaunchAnimator"); 46 static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect"); 47 static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks"); 48 private final LauncherInstrumentation mLauncher; 49 @NonNull 50 private final UiObject2 mTask; 51 private final TaskViewType mType; 52 private final BaseOverview mOverview; 53 OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview)54 OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) { 55 mLauncher = launcher; 56 mLauncher.assertNotNull("task must not be null", task); 57 mTask = task; 58 mOverview = overview; 59 mType = getType(task); 60 verifyActiveContainer(); 61 } 62 verifyActiveContainer()63 private void verifyActiveContainer() { 64 mOverview.verifyActiveContainer(); 65 } 66 67 /** 68 * Returns the height of the visible task, or the combined height of two tasks in split with a 69 * divider between. 70 */ getVisibleHeight()71 int getVisibleHeight() { 72 if (isGrouped()) { 73 return getCombinedSplitTaskHeight(); 74 } 75 76 UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes); 77 return taskSnapshot1.getVisibleBounds().height(); 78 } 79 80 /** 81 * Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider. 82 */ getCombinedSplitTaskHeight()83 private int getCombinedSplitTaskHeight() { 84 UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); 85 UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); 86 87 // If the split task is partly off screen, taskSnapshot1 can be invisible. 88 if (taskSnapshot1 == null) { 89 return taskSnapshot2.getVisibleBounds().height(); 90 } 91 92 int top = Math.min( 93 taskSnapshot1.getVisibleBounds().top, taskSnapshot2.getVisibleBounds().top); 94 int bottom = Math.max( 95 taskSnapshot1.getVisibleBounds().bottom, taskSnapshot2.getVisibleBounds().bottom); 96 97 return bottom - top; 98 } 99 100 /** 101 * Returns the width of the visible task, or the combined width of two tasks in split with a 102 * divider between. 103 */ getVisibleWidth()104 int getVisibleWidth() { 105 if (isGrouped()) { 106 return getCombinedSplitTaskWidth(); 107 } 108 109 UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes); 110 return taskSnapshot1.getVisibleBounds().width(); 111 } 112 113 /** 114 * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider. 115 */ getCombinedSplitTaskWidth()116 private int getCombinedSplitTaskWidth() { 117 UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); 118 UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); 119 120 int left = Math.min( 121 taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left); 122 int right = Math.max( 123 taskSnapshot1.getVisibleBounds().right, taskSnapshot2.getVisibleBounds().right); 124 125 return right - left; 126 } 127 getTaskCenterX()128 public int getTaskCenterX() { 129 return mTask.getVisibleCenter().x; 130 } 131 getTaskCenterY()132 public int getTaskCenterY() { 133 return mTask.getVisibleCenter().y; 134 } 135 getExactCenterX()136 float getExactCenterX() { 137 return mTask.getVisibleBounds().exactCenterX(); 138 } 139 getUiObject()140 UiObject2 getUiObject() { 141 return mTask; 142 } 143 144 /** 145 * Dismisses the task by swiping up. 146 */ dismiss()147 public void dismiss() { 148 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 149 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 150 "want to dismiss an overview task")) { 151 verifyActiveContainer(); 152 int taskCountBeforeDismiss = mOverview.getTaskCount(); 153 mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss); 154 if (taskCountBeforeDismiss == 1) { 155 dismissBySwipingUp(); 156 return; 157 } 158 159 boolean taskWasFocused = mLauncher.isTablet() 160 && !isDesktop() 161 && getVisibleHeight() == mLauncher.getOverviewTaskSize().height(); 162 List<Integer> originalTasksCenterX = 163 getCurrentTasksCenterXList().stream().sorted().toList(); 164 boolean isClearAllVisibleBeforeDismiss = mOverview.isClearAllVisible(); 165 166 dismissBySwipingUp(); 167 168 long numNonDesktopTasks = mOverview.getCurrentTasksForTablet() 169 .stream().filter(t -> !t.isDesktop()).count(); 170 171 try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("dismissed")) { 172 if (taskWasFocused && numNonDesktopTasks > 0) { 173 mLauncher.assertNotNull("No task became focused", 174 mOverview.getFocusedTaskForTablet()); 175 } 176 if (!isClearAllVisibleBeforeDismiss) { 177 List<Integer> currentTasksCenterX = 178 getCurrentTasksCenterXList().stream().sorted().toList(); 179 if (originalTasksCenterX.size() == currentTasksCenterX.size()) { 180 // Check for the same number of visible tasks before and after to 181 // avoid asserting on cases of shifting all tasks to close the distance 182 // between clear all and tasks at the end of the grid. 183 mLauncher.assertTrue("Task centers not aligned", 184 originalTasksCenterX.equals(currentTasksCenterX)); 185 } 186 } 187 } 188 } 189 } 190 dismissBySwipingUp()191 private void dismissBySwipingUp() { 192 verifyActiveContainer(); 193 // Dismiss the task via flinging it up. 194 final Rect taskBounds = mLauncher.getVisibleBounds(mTask); 195 final int centerX = taskBounds.centerX(); 196 final int centerY = taskBounds.bottom - 1; 197 mLauncher.executeAndWaitForLauncherEvent( 198 () -> mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false, 199 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER), 200 event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(event.getClassName()), 201 () -> "Didn't receive a dismiss animation ends message: " + centerX + ", " 202 + centerY, "swiping to dismiss"); 203 } 204 getCurrentTasksCenterXList()205 private List<Integer> getCurrentTasksCenterXList() { 206 return mLauncher.isTablet() 207 ? mOverview.getCurrentTasksForTablet().stream() 208 .map(OverviewTask::getTaskCenterX) 209 .collect(Collectors.toList()) 210 : List.of(mOverview.getCurrentTask().getTaskCenterX()); 211 } 212 213 /** 214 * Starts dismissing the task by swiping up, then cancels, and task springs back to start. 215 */ dismissCancel()216 public void dismissCancel() { 217 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 218 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 219 "want to start dismissing an overview task then cancel")) { 220 verifyActiveContainer(); 221 int taskCountBeforeDismiss = mOverview.getTaskCount(); 222 mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss); 223 224 final Rect taskBounds = mLauncher.getVisibleBounds(mTask); 225 final int centerX = taskBounds.centerX(); 226 final int centerY = taskBounds.bottom - 1; 227 final int endCenterY = centerY - (taskBounds.height() / 4); 228 mLauncher.executeAndWaitForLauncherEvent( 229 // Set slowDown to true so we do not fling the task at the end of the drag, as 230 // we want it to cancel and return back to the origin. We use 30 steps to 231 // perform the gesture slowly as well, to avoid flinging. 232 () -> mLauncher.linearGesture(centerX, centerY, centerX, endCenterY, 233 /* steps= */ 30, /* slowDown= */ true, 234 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER), 235 event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals( 236 event.getClassName()), 237 () -> "Canceling swipe to dismiss did not end with task at origin.", 238 "cancel swiping to dismiss"); 239 240 } 241 } 242 243 /** 244 * Clicks the task. 245 */ open()246 public LaunchedAppState open() { 247 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 248 verifyActiveContainer(); 249 mLauncher.executeAndWaitForLauncherStop( 250 () -> mLauncher.clickLauncherObject(mTask), 251 "clicking an overview task"); 252 if (mOverview.getContainerType() 253 == LauncherInstrumentation.ContainerType.SPLIT_SCREEN_SELECT) { 254 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SPLIT_START_EVENT); 255 256 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 257 "launched splitscreen")) { 258 259 BySelector divider = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle"); 260 mLauncher.waitForSystemUiObject(divider); 261 return new LaunchedAppState(mLauncher); 262 } 263 } else { 264 final Pattern event; 265 if (mOverview.isLiveTile(mTask)) { 266 event = TASK_START_EVENT_LIVE_TILE; 267 } else if (mType == TaskViewType.DESKTOP) { 268 event = TASK_START_EVENT_DESKTOP; 269 } else { 270 event = TASK_START_EVENT; 271 } 272 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event); 273 274 if (mType == TaskViewType.DESKTOP) { 275 try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer( 276 "launched desktop")) { 277 mLauncher.waitForSystemUiObject("desktop_mode_caption"); 278 } 279 } 280 return new LaunchedAppState(mLauncher); 281 } 282 } 283 } 284 285 /** Taps the task menu. Returns the task menu object. */ 286 @NonNull tapMenu()287 public OverviewTaskMenu tapMenu() { 288 return tapMenu(DEFAULT); 289 } 290 291 /** Taps the task menu of the split task. Returns the split task's menu object. */ 292 @NonNull tapMenu(OverviewTaskContainer task)293 public OverviewTaskMenu tapMenu(OverviewTaskContainer task) { 294 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 295 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 296 "want to tap the task menu")) { 297 mLauncher.clickLauncherObject( 298 mLauncher.waitForObjectInContainer(mTask, task.iconAppRes)); 299 300 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 301 "tapped the task menu")) { 302 return new OverviewTaskMenu(mLauncher); 303 } 304 } 305 } 306 findObjectInTask(String resName)307 private UiObject2 findObjectInTask(String resName) { 308 return mTask.findObject(mLauncher.getOverviewObjectSelector(resName)); 309 } 310 311 /** 312 * Returns whether the given String is contained in this Task's contentDescription. Also returns 313 * true if both Strings are null. 314 */ containsContentDescription(String expected, OverviewTaskContainer overviewTaskContainer)315 public boolean containsContentDescription(String expected, 316 OverviewTaskContainer overviewTaskContainer) { 317 String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription(); 318 if (actual == null && expected == null) { 319 return true; 320 } 321 if (actual == null || expected == null) { 322 return false; 323 } 324 return actual.contains(expected); 325 } 326 327 /** 328 * Returns whether the given String is contained in this Task's contentDescription. Also returns 329 * true if both Strings are null 330 */ containsContentDescription(String expected)331 public boolean containsContentDescription(String expected) { 332 return containsContentDescription(expected, DEFAULT); 333 } 334 335 /** 336 * Returns the TaskView type of the task. It will return whether the task is a single TaskView, 337 * a GroupedTaskView or a DesktopTaskView. 338 */ getType(UiObject2 task)339 static TaskViewType getType(UiObject2 task) { 340 String resourceName = task.getResourceName(); 341 if (resourceName.endsWith("task_view_grouped")) { 342 return TaskViewType.GROUPED; 343 } else if (resourceName.endsWith("task_view_desktop")) { 344 return TaskViewType.DESKTOP; 345 } else { 346 return TaskViewType.SINGLE; 347 } 348 } 349 isGrouped()350 boolean isGrouped() { 351 return mType == TaskViewType.GROUPED; 352 } 353 isDesktop()354 public boolean isDesktop() { 355 return mType == TaskViewType.DESKTOP; 356 } 357 358 /** 359 * Enum used to specify which resource name should be used depending on the type of the task. 360 */ 361 public enum OverviewTaskContainer { 362 // The main task when the task is not split. 363 DEFAULT("snapshot", "icon"), 364 // The first task in split task. 365 SPLIT_TOP_OR_LEFT("snapshot", "icon"), 366 // The second task in split task. 367 SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"), 368 // The desktop task. 369 DESKTOP("background", "icon"); 370 371 public final String snapshotRes; 372 public final String iconAppRes; 373 OverviewTaskContainer(String snapshotRes, String iconAppRes)374 OverviewTaskContainer(String snapshotRes, String iconAppRes) { 375 this.snapshotRes = snapshotRes; 376 this.iconAppRes = iconAppRes; 377 } 378 } 379 380 enum TaskViewType { 381 SINGLE, 382 GROUPED, 383 DESKTOP 384 } 385 } 386