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 import static android.view.KeyEvent.KEYCODE_META_RIGHT; 21 22 import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL; 23 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; 24 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL; 25 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.os.Bundle; 29 import android.widget.TextView; 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.StaleObjectException; 37 import androidx.test.uiautomator.UiObject2; 38 39 import com.android.launcher3.testing.shared.TestProtocol; 40 41 import java.util.Collections; 42 import java.util.Comparator; 43 import java.util.List; 44 import java.util.regex.Pattern; 45 import java.util.stream.Collectors; 46 47 /** 48 * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview. 49 */ 50 public abstract class AllApps extends LauncherInstrumentation.VisibleContainer 51 implements KeyboardQuickSwitchSource { 52 // Defer updates flag used to defer all apps updates by a test's request. 53 private static final int DEFER_UPDATES_TEST = 1 << 1; 54 55 private static final int MAX_SCROLL_ATTEMPTS = 40; 56 57 private static final String BOTTOM_SHEET_RES_ID = "bottom_sheet_background"; 58 private static final String FAST_SCROLLER_RES_ID = "fast_scroller"; 59 private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile( 60 "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0"); 61 private static final String UNLOCK_BUTTON_VIEW_RES_ID = "ps_lock_unlock_button"; 62 63 private final int mHeight; 64 private final int mIconHeight; 65 AllApps(LauncherInstrumentation launcher)66 AllApps(LauncherInstrumentation launcher) { 67 super(launcher); 68 final UiObject2 allAppsContainer = verifyActiveContainer(); 69 mHeight = mLauncher.getVisibleBounds(allAppsContainer).height(); 70 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 71 // Wait for the recycler to populate. 72 mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class)); 73 verifyNotFrozen("All apps freeze flags upon opening all apps"); 74 mIconHeight = mLauncher.getTestInfo(TestProtocol.REQUEST_ICON_HEIGHT) 75 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 76 } 77 78 @Override getLauncher()79 public LauncherInstrumentation getLauncher() { 80 return mLauncher; 81 } 82 83 @Override getStartingContainerType()84 public LauncherInstrumentation.ContainerType getStartingContainerType() { 85 return getContainerType(); 86 } 87 hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler, BySelector appIconSelector, int displayBottom)88 private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler, 89 BySelector appIconSelector, int displayBottom) { 90 final UiObject2 icon; 91 try { 92 icon = appListRecycler.findObject(appIconSelector); 93 } catch (StaleObjectException e) { 94 mLauncher.fail("All apps recycler disappeared from screen"); 95 return false; 96 } 97 if (icon == null) { 98 LauncherInstrumentation.log("hasClickableIcon: icon not visible"); 99 return false; 100 } 101 final Rect iconBounds = mLauncher.getVisibleBounds(icon); 102 LauncherInstrumentation.log("hasClickableIcon: icon bounds: " + iconBounds); 103 if (iconBounds.height() < mIconHeight / 2) { 104 LauncherInstrumentation.log("hasClickableIcon: icon has insufficient height"); 105 return false; 106 } 107 if (hasSearchBox() && iconCenterInSearchBox(allAppsContainer, icon)) { 108 LauncherInstrumentation.log("hasClickableIcon: icon center is under search box"); 109 return false; 110 } 111 if (iconCenterInRecyclerTopPadding(appListRecycler, icon)) { 112 LauncherInstrumentation.log( 113 "hasClickableIcon: icon center is under the app list recycler's top padding."); 114 return false; 115 } 116 if (iconBounds.bottom > displayBottom) { 117 LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset"); 118 return false; 119 } 120 LauncherInstrumentation.log("hasClickableIcon: icon is clickable"); 121 return true; 122 } 123 iconCenterInSearchBox(UiObject2 allAppsContainer, UiObject2 icon)124 private boolean iconCenterInSearchBox(UiObject2 allAppsContainer, UiObject2 icon) { 125 final Point iconCenter = icon.getVisibleCenter(); 126 return mLauncher.getVisibleBounds(getSearchBox(allAppsContainer)).contains( 127 iconCenter.x, iconCenter.y); 128 } 129 iconCenterInRecyclerTopPadding(UiObject2 appsListRecycler, UiObject2 icon)130 private boolean iconCenterInRecyclerTopPadding(UiObject2 appsListRecycler, UiObject2 icon) { 131 final Point iconCenter = icon.getVisibleCenter(); 132 133 return iconCenter.y <= mLauncher.getVisibleBounds(appsListRecycler).top 134 + getAppsListRecyclerTopPadding(); 135 } 136 137 /** 138 * Finds an icon. If the icon doesn't exist, return null. 139 * Scrolls the app list when needed to make sure the icon is visible. 140 * 141 * @param appName name of the app. 142 * @return The app if found, and null if not found. 143 */ 144 @Nullable tryGetAppIcon(String appName)145 public AppIcon tryGetAppIcon(String appName) { 146 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 147 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 148 "getting app icon " + appName + " on all apps")) { 149 final UiObject2 allAppsContainer = verifyActiveContainer(); 150 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 151 152 int deviceHeight = mLauncher.getRealDisplaySize().y; 153 int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen(); 154 final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher); 155 if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector, 156 bottomGestureStartOnScreen)) { 157 scrollBackToBeginning(); 158 int attempts = 0; 159 int scroll = getAllAppsScroll(); 160 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) { 161 while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector, 162 bottomGestureStartOnScreen)) { 163 mLauncher.scrollToLastVisibleRow( 164 allAppsContainer, 165 getBottomVisibleIconBounds(allAppsContainer), 166 mLauncher.getVisibleBounds(appListRecycler).top 167 + getAppsListRecyclerTopPadding() 168 - mLauncher.getVisibleBounds(allAppsContainer).top, 169 getAppsListRecyclerBottomPadding()); 170 verifyActiveContainer(); 171 final int newScroll = getAllAppsScroll(); 172 LauncherInstrumentation.log( 173 String.format("tryGetAppIcon: scrolled from %d to %d", scroll, 174 newScroll)); 175 mLauncher.assertTrue( 176 "Scrolled in a wrong direction in AllApps: from " + scroll + " to " 177 + newScroll, newScroll >= scroll); 178 if (newScroll == scroll) break; 179 180 mLauncher.assertTrue( 181 "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS, 182 ++attempts <= MAX_SCROLL_ATTEMPTS); 183 scroll = newScroll; 184 } 185 } 186 verifyActiveContainer(); 187 } 188 // Ignore bottom offset selection here as there might not be any scroll more scroll 189 // region available. 190 if (hasClickableIcon( 191 allAppsContainer, appListRecycler, appIconSelector, deviceHeight)) { 192 193 final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler, 194 appIconSelector); 195 return createAppIcon(appIcon); 196 } else { 197 return null; 198 } 199 } 200 } 201 202 /** @return visible bounds of the top-most visible icon in the container. */ getTopVisibleIconBounds(UiObject2 allAppsContainer)203 protected Rect getTopVisibleIconBounds(UiObject2 allAppsContainer) { 204 return mLauncher.getVisibleBounds(Collections.min(getVisibleIcons(allAppsContainer), 205 Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top))); 206 } 207 208 /** @return visible bounds of the bottom-most visible icon in the container. */ getBottomVisibleIconBounds(UiObject2 allAppsContainer)209 protected Rect getBottomVisibleIconBounds(UiObject2 allAppsContainer) { 210 return mLauncher.getVisibleBounds(Collections.max(getVisibleIcons(allAppsContainer), 211 Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top))); 212 } 213 214 @NonNull getVisibleIcons(UiObject2 allAppsContainer)215 private List<UiObject2> getVisibleIcons(UiObject2 allAppsContainer) { 216 return mLauncher.getObjectsInContainer(allAppsContainer, "icon") 217 .stream() 218 .filter(icon -> 219 mLauncher.getVisibleBounds(icon).top 220 < mLauncher.getBottomGestureStartOnScreen()) 221 .collect(Collectors.toList()); 222 } 223 224 /** 225 * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make 226 * sure the icon is visible. 227 * 228 * @param appName name of the app. 229 * @return The app. 230 */ 231 @NonNull getAppIcon(String appName)232 public AppIcon getAppIcon(String appName) { 233 AppIcon appIcon = tryGetAppIcon(appName); 234 mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon); 235 // appIcon.getAppName() checks for content description, so it is possible that it can have 236 // trailing words. So check if the content description contains the appName. 237 mLauncher.assertTrue("Wrong app icon name.", appIcon.getAppName().contains(appName)); 238 return appIcon; 239 } 240 241 @NonNull createAppIcon(UiObject2 icon)242 protected abstract AppIcon createAppIcon(UiObject2 icon); 243 hasSearchBox()244 protected abstract boolean hasSearchBox(); 245 getAppsListRecyclerTopPadding()246 protected abstract int getAppsListRecyclerTopPadding(); 247 getAppsListRecyclerBottomPadding()248 protected int getAppsListRecyclerBottomPadding() { 249 return mLauncher.getTestInfo(TestProtocol.REQUEST_ALL_APPS_BOTTOM_PADDING) 250 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 251 } 252 scrollBackToBeginning()253 private void scrollBackToBeginning() { 254 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 255 "want to scroll back in all apps")) { 256 LauncherInstrumentation.log("Scrolling to the beginning"); 257 final UiObject2 allAppsContainer = verifyActiveContainer(); 258 259 int attempts = 0; 260 final Rect margins = new Rect( 261 /* left= */ 0, 262 mHeight / 2, 263 /* right= */ 0, 264 /* bottom= */ getAppsListRecyclerBottomPadding()); 265 266 for (int scroll = getAllAppsScroll(); 267 scroll != 0; 268 scroll = getAllAppsScroll()) { 269 mLauncher.assertTrue("Negative scroll position", scroll > 0); 270 271 mLauncher.assertTrue( 272 "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS, 273 ++attempts <= MAX_SCROLL_ATTEMPTS); 274 275 mLauncher.scroll( 276 allAppsContainer, 277 Direction.UP, 278 margins, 279 /* steps= */ 12, 280 /* slowDown= */ false); 281 } 282 283 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled up")) { 284 verifyActiveContainer(); 285 } 286 } 287 } 288 getAllAppsScroll()289 protected abstract int getAllAppsScroll(); 290 getAppListRecycler(UiObject2 allAppsContainer)291 protected UiObject2 getAppListRecycler(UiObject2 allAppsContainer) { 292 return mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view"); 293 } 294 getAllAppsHeader(UiObject2 allAppsContainer)295 protected UiObject2 getAllAppsHeader(UiObject2 allAppsContainer) { 296 return mLauncher.waitForObjectInContainer(allAppsContainer, "all_apps_header"); 297 } 298 getSearchBox(UiObject2 allAppsContainer)299 protected UiObject2 getSearchBox(UiObject2 allAppsContainer) { 300 return mLauncher.waitForObjectInContainer(allAppsContainer, "search_container_all_apps"); 301 } 302 303 /** 304 * Flings forward (down) and waits the fling's end. 305 */ flingForward()306 public void flingForward() { 307 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 308 LauncherInstrumentation.Closable c = 309 mLauncher.addContextLayer("want to fling forward in all apps")) { 310 final UiObject2 allAppsContainer = verifyActiveContainer(); 311 // Start the gesture in the center to avoid starting at elements near the top. 312 mLauncher.scroll( 313 allAppsContainer, 314 Direction.DOWN, 315 new Rect(0, 0, 0, mHeight / 2), 316 /* steps= */ 10, 317 /* slowDown= */ false); 318 verifyActiveContainer(); 319 } 320 } 321 322 /** 323 * Flings backward (up) and waits the fling's end. 324 */ flingBackward()325 public void flingBackward() { 326 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 327 LauncherInstrumentation.Closable c = 328 mLauncher.addContextLayer("want to fling backward in all apps")) { 329 final UiObject2 allAppsContainer = verifyActiveContainer(); 330 // Start the gesture in the center, for symmetry with forward. 331 mLauncher.scroll( 332 allAppsContainer, 333 Direction.UP, 334 new Rect(0, mHeight / 2, 0, 0), 335 /* steps= */ 10, 336 /*slowDown= */ false); 337 verifyActiveContainer(); 338 } 339 } 340 341 /** 342 * Freezes updating app list upon app install/uninstall/update. 343 */ freeze()344 public void freeze() { 345 mLauncher.getTestInfo(TestProtocol.REQUEST_FREEZE_APP_LIST); 346 } 347 348 /** 349 * Resumes updating app list upon app install/uninstall/update. 350 */ unfreeze()351 public void unfreeze() { 352 mLauncher.getTestInfo(TestProtocol.REQUEST_UNFREEZE_APP_LIST); 353 } 354 verifyNotFrozen(String message)355 private void verifyNotFrozen(String message) { 356 mLauncher.assertEquals(message, 0, getFreezeFlags() & DEFER_UPDATES_TEST); 357 mLauncher.assertTrue(message, mLauncher.waitAndGet(() -> getFreezeFlags() == 0, 358 WAIT_TIME_MS, DEFAULT_POLL_INTERVAL)); 359 } 360 getFreezeFlags()361 private int getFreezeFlags() { 362 final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS); 363 return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 364 } 365 366 /** 367 * Taps outside bottom sheet to dismiss it. Available on tablets only. 368 * 369 * @param tapRight Tap on the right of bottom sheet if true, or left otherwise. 370 */ dismissByTappingOutsideForTablet(boolean tapRight)371 public void dismissByTappingOutsideForTablet(boolean tapRight) { 372 mLauncher.assertTrue("Device must be a tablet", mLauncher.isTablet()); 373 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 374 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 375 "want to tap outside AllApps bottom sheet on the " 376 + (tapRight ? "right" : "left"))) { 377 378 final UiObject2 container = (tapRight) 379 ? mLauncher.waitForLauncherObject(FAST_SCROLLER_RES_ID) : 380 mLauncher.waitForLauncherObject(BOTTOM_SHEET_RES_ID); 381 382 touchOutside(tapRight, container); 383 try (LauncherInstrumentation.Closable tapped = mLauncher.addContextLayer( 384 "tapped outside AllApps bottom sheet")) { 385 verifyVisibleContainerOnDismiss(); 386 } 387 } 388 } 389 touchOutside(boolean tapRight, UiObject2 container)390 protected void touchOutside(boolean tapRight, UiObject2 container) { 391 mLauncher.touchOutsideContainer(container, tapRight, false); 392 } 393 394 /** Presses the meta keyboard shortcut to dismiss AllApps. */ dismissByKeyboardShortcut()395 public void dismissByKeyboardShortcut() { 396 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 397 pressMetaKey(); 398 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 399 "pressed meta key")) { 400 verifyVisibleContainerOnDismiss(); 401 } 402 } 403 } 404 pressMetaKey()405 protected void pressMetaKey() { 406 mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT); 407 } 408 409 /** Presses the esc key to dismiss AllApps. */ dismissByEscKey()410 public void dismissByEscKey() { 411 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) { 412 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP); 413 mLauncher.runToState( 414 () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE), 415 NORMAL_STATE_ORDINAL, 416 "pressing esc key"); 417 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 418 "pressed esc key")) { 419 verifyVisibleContainerOnDismiss(); 420 } 421 } 422 } 423 424 /** Returns PredictionRow if present in view. */ 425 @NonNull getPredictionRowView()426 public PredictionRow getPredictionRowView() { 427 final UiObject2 allAppsContainer = verifyActiveContainer(); 428 final UiObject2 allAppsHeader = getAllAppsHeader(allAppsContainer); 429 return new PredictionRow(mLauncher, allAppsHeader); 430 } 431 432 /** Returns PrivateSpaceContainer if present in view. */ 433 @NonNull getPrivateSpaceUnlockedView()434 public PrivateSpaceContainer getPrivateSpaceUnlockedView() { 435 final UiObject2 allAppsContainer = verifyActiveContainer(); 436 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 437 return new PrivateSpaceContainer(mLauncher, appListRecycler, this, true); 438 } 439 440 /** Returns PrivateSpaceContainer in locked state, if present in view. */ 441 @NonNull getPrivateSpaceLockedView()442 public PrivateSpaceContainer getPrivateSpaceLockedView() { 443 final UiObject2 allAppsContainer = verifyActiveContainer(); 444 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 445 return new PrivateSpaceContainer(mLauncher, appListRecycler, this, false); 446 } 447 448 /** 449 * Toggles Lock/Unlock of Private Space, changing the All Apps Ui. 450 */ togglePrivateSpace()451 public void togglePrivateSpace() { 452 final UiObject2 allAppsContainer = verifyActiveContainer(); 453 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 454 UiObject2 unLockButtonView = mLauncher.waitForObjectInContainer(appListRecycler, 455 UNLOCK_BUTTON_VIEW_RES_ID); 456 mLauncher.waitForObjectEnabled(unLockButtonView, "Private Space Unlock Button"); 457 mLauncher.assertTrue("PS Unlock Button is non-clickable", unLockButtonView.isClickable()); 458 unLockButtonView.click(); 459 } 460 verifyVisibleContainerOnDismiss()461 protected abstract void verifyVisibleContainerOnDismiss(); 462 463 /** 464 * Return the QSB UI object on the AllApps screen. 465 * 466 * @return the QSB UI object. 467 */ 468 @NonNull getQsb()469 public abstract Qsb getQsb(); 470 }