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.annotation.Nullable; 28 import androidx.test.uiautomator.By; 29 import androidx.test.uiautomator.BySelector; 30 import androidx.test.uiautomator.UiObject2; 31 32 import com.android.launcher3.testing.shared.TestProtocol; 33 34 import java.util.List; 35 import java.util.regex.Pattern; 36 import java.util.stream.Collectors; 37 38 /** 39 * A recent task in the overview panel carousel. 40 */ 41 public final class OverviewTask { 42 private static final String SYSTEMUI_PACKAGE = "com.android.systemui"; 43 static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync"); 44 static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents"); 45 static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile( 46 "composeRecentsLaunchAnimator"); 47 static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect"); 48 static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks"); 49 private final LauncherInstrumentation mLauncher; 50 @NonNull 51 private final UiObject2 mTask; 52 private final TaskViewType mType; 53 private final BaseOverview mOverview; 54 OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview)55 OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) { 56 mLauncher = launcher; 57 mLauncher.assertNotNull("task must not be null", task); 58 mTask = task; 59 mOverview = overview; 60 mType = getType(task); 61 verifyActiveContainer(); 62 } 63 verifyActiveContainer()64 private void verifyActiveContainer() { 65 mOverview.verifyActiveContainer(); 66 } 67 68 /** 69 * Returns the height of the visible task, or the combined height of two tasks in split with a 70 * divider between. 71 */ getVisibleHeight()72 int getVisibleHeight() { 73 if (isGrouped()) { 74 return getCombinedSplitTaskHeight(); 75 } 76 77 UiObject2 taskSnapshot1 = findObjectInTask((isDesktop() ? DESKTOP : DEFAULT).snapshotRes); 78 return taskSnapshot1.getVisibleBounds().height(); 79 } 80 81 /** 82 * Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider. 83 */ getCombinedSplitTaskHeight()84 private int getCombinedSplitTaskHeight() { 85 UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); 86 UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); 87 88 // If the split task is partly off screen, taskSnapshot1 can be invisible. 89 if (taskSnapshot1 == null) { 90 return taskSnapshot2.getVisibleBounds().height(); 91 } 92 93 int top = Math.min( 94 taskSnapshot1.getVisibleBounds().top, taskSnapshot2.getVisibleBounds().top); 95 int bottom = Math.max( 96 taskSnapshot1.getVisibleBounds().bottom, taskSnapshot2.getVisibleBounds().bottom); 97 98 return bottom - top; 99 } 100 101 /** 102 * Returns the width of the visible task, or the combined width of two tasks in split with a 103 * divider between. 104 */ getVisibleWidth()105 int getVisibleWidth() { 106 if (isGrouped()) { 107 return getCombinedSplitTaskWidth(); 108 } 109 110 UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes); 111 return taskSnapshot1.getVisibleBounds().width(); 112 } 113 114 /** 115 * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider. 116 */ getCombinedSplitTaskWidth()117 private int getCombinedSplitTaskWidth() { 118 UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes); 119 UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes); 120 121 int left = Math.min( 122 taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left); 123 int right = Math.max( 124 taskSnapshot1.getVisibleBounds().right, taskSnapshot2.getVisibleBounds().right); 125 126 return right - left; 127 } 128 getTaskCenterX()129 int getTaskCenterX() { 130 return mTask.getVisibleCenter().x; 131 } 132 getTaskCenterY()133 int getTaskCenterY() { 134 return mTask.getVisibleCenter().y; 135 } 136 getExactCenterX()137 float getExactCenterX() { 138 return mTask.getVisibleBounds().exactCenterX(); 139 } 140 getUiObject()141 UiObject2 getUiObject() { 142 return mTask; 143 } 144 145 /** 146 * Dismisses the task by swiping up. 147 */ dismiss()148 public void dismiss() { 149 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 150 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 151 "want to dismiss an overview task")) { 152 verifyActiveContainer(); 153 int taskCountBeforeDismiss = mOverview.getTaskCount(); 154 mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss); 155 if (taskCountBeforeDismiss == 1) { 156 dismissBySwipingUp(); 157 return; 158 } 159 160 boolean taskWasFocused = mLauncher.isTablet() 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 * Clicks the task. 215 */ open()216 public LaunchedAppState open() { 217 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 218 verifyActiveContainer(); 219 mLauncher.executeAndWaitForLauncherStop( 220 () -> mLauncher.clickLauncherObject(mTask), 221 "clicking an overview task"); 222 if (mOverview.getContainerType() 223 == LauncherInstrumentation.ContainerType.SPLIT_SCREEN_SELECT) { 224 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SPLIT_START_EVENT); 225 226 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 227 "launched splitscreen")) { 228 229 BySelector divider = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle"); 230 mLauncher.waitForSystemUiObject(divider); 231 return new LaunchedAppState(mLauncher); 232 } 233 } else { 234 final Pattern event; 235 if (mOverview.isLiveTile(mTask)) { 236 event = TASK_START_EVENT_LIVE_TILE; 237 } else if (mType == TaskViewType.DESKTOP) { 238 event = TASK_START_EVENT_DESKTOP; 239 } else { 240 event = TASK_START_EVENT; 241 } 242 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event); 243 244 if (mType == TaskViewType.DESKTOP) { 245 try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer( 246 "launched desktop")) { 247 mLauncher.waitForSystemUiObject("desktop_mode_caption"); 248 } 249 } 250 return new LaunchedAppState(mLauncher); 251 } 252 } 253 } 254 255 /** Taps the task menu. Returns the task menu object. */ 256 @NonNull tapMenu()257 public OverviewTaskMenu tapMenu() { 258 return tapMenu(DEFAULT); 259 } 260 261 /** Taps the task menu of the split task. Returns the split task's menu object. */ 262 @NonNull tapMenu(OverviewTaskContainer task)263 public OverviewTaskMenu tapMenu(OverviewTaskContainer task) { 264 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 265 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 266 "want to tap the task menu")) { 267 mLauncher.clickLauncherObject( 268 mLauncher.waitForObjectInContainer(mTask, task.iconAppRes)); 269 270 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( 271 "tapped the task menu")) { 272 return new OverviewTaskMenu(mLauncher); 273 } 274 } 275 } 276 findObjectInTask(String resName)277 private UiObject2 findObjectInTask(String resName) { 278 return mTask.findObject(mLauncher.getOverviewObjectSelector(resName)); 279 } 280 281 /** 282 * Returns whether the given String is contained in this Task's contentDescription. Also returns 283 * true if both Strings are null. 284 * 285 * TODO(b/342627272): remove Nullable support once the bug causing it to be null is fixed. 286 */ containsContentDescription(@ullable String expected, OverviewTaskContainer overviewTaskContainer)287 public boolean containsContentDescription(@Nullable String expected, 288 OverviewTaskContainer overviewTaskContainer) { 289 String actual = findObjectInTask(overviewTaskContainer.snapshotRes).getContentDescription(); 290 if (actual == null && expected == null) { 291 return true; 292 } 293 if (actual == null || expected == null) { 294 return false; 295 } 296 return actual.contains(expected); 297 } 298 299 /** 300 * Returns whether the given String is contained in this Task's contentDescription. Also returns 301 * true if both Strings are null 302 */ containsContentDescription(@ullable String expected)303 public boolean containsContentDescription(@Nullable String expected) { 304 return containsContentDescription(expected, DEFAULT); 305 } 306 307 /** 308 * Returns the TaskView type of the task. It will return whether the task is a single TaskView, 309 * a GroupedTaskView or a DesktopTaskView. 310 */ getType(UiObject2 task)311 static TaskViewType getType(UiObject2 task) { 312 String resourceName = task.getResourceName(); 313 if (resourceName.endsWith("task_view_grouped")) { 314 return TaskViewType.GROUPED; 315 } else if (resourceName.endsWith("task_view_desktop")) { 316 return TaskViewType.DESKTOP; 317 } else { 318 return TaskViewType.SINGLE; 319 } 320 } 321 isGrouped()322 boolean isGrouped() { 323 return mType == TaskViewType.GROUPED; 324 } 325 isDesktop()326 public boolean isDesktop() { 327 return mType == TaskViewType.DESKTOP; 328 } 329 330 /** 331 * Enum used to specify which resource name should be used depending on the type of the task. 332 */ 333 public enum OverviewTaskContainer { 334 // The main task when the task is not split. 335 DEFAULT("snapshot", "icon"), 336 // The first task in split task. 337 SPLIT_TOP_OR_LEFT("snapshot", "icon"), 338 // The second task in split task. 339 SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon"), 340 // The desktop task. 341 DESKTOP("background", "icon"); 342 343 public final String snapshotRes; 344 public final String iconAppRes; 345 OverviewTaskContainer(String snapshotRes, String iconAppRes)346 OverviewTaskContainer(String snapshotRes, String iconAppRes) { 347 this.snapshotRes = snapshotRes; 348 this.iconAppRes = iconAppRes; 349 } 350 } 351 352 enum TaskViewType { 353 SINGLE, 354 GROUPED, 355 DESKTOP 356 } 357 } 358