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.LauncherInstrumentation.DEFAULT_POLL_INTERVAL; 20 import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; 21 22 import android.graphics.Point; 23 import android.graphics.Rect; 24 import android.os.Bundle; 25 import android.widget.TextView; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.test.uiautomator.By; 30 import androidx.test.uiautomator.BySelector; 31 import androidx.test.uiautomator.Direction; 32 import androidx.test.uiautomator.StaleObjectException; 33 import androidx.test.uiautomator.UiObject2; 34 35 import com.android.launcher3.testing.shared.TestProtocol; 36 37 import java.util.Collections; 38 import java.util.Comparator; 39 import java.util.List; 40 import java.util.stream.Collectors; 41 42 /** 43 * Operations on AllApps opened from Home. Also a parent for All Apps opened from Overview. 44 */ 45 public abstract class AllApps extends LauncherInstrumentation.VisibleContainer { 46 // Defer updates flag used to defer all apps updates by a test's request. 47 private static final int DEFER_UPDATES_TEST = 1 << 1; 48 49 private static final int MAX_SCROLL_ATTEMPTS = 40; 50 51 private final int mHeight; 52 private final int mIconHeight; 53 AllApps(LauncherInstrumentation launcher)54 AllApps(LauncherInstrumentation launcher) { 55 super(launcher); 56 final UiObject2 allAppsContainer = verifyActiveContainer(); 57 mHeight = mLauncher.getVisibleBounds(allAppsContainer).height(); 58 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 59 // Wait for the recycler to populate. 60 mLauncher.waitForObjectInContainer(appListRecycler, By.clazz(TextView.class)); 61 verifyNotFrozen("All apps freeze flags upon opening all apps"); 62 mIconHeight = mLauncher.getTestInfo(TestProtocol.REQUEST_ICON_HEIGHT) 63 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 64 } 65 hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler, BySelector appIconSelector, int displayBottom)66 private boolean hasClickableIcon(UiObject2 allAppsContainer, UiObject2 appListRecycler, 67 BySelector appIconSelector, int displayBottom) { 68 final UiObject2 icon; 69 try { 70 icon = appListRecycler.findObject(appIconSelector); 71 } catch (StaleObjectException e) { 72 mLauncher.fail("All apps recycler disappeared from screen"); 73 return false; 74 } 75 if (icon == null) { 76 LauncherInstrumentation.log("hasClickableIcon: icon not visible"); 77 return false; 78 } 79 final Rect iconBounds = mLauncher.getVisibleBounds(icon); 80 LauncherInstrumentation.log("hasClickableIcon: icon bounds: " + iconBounds); 81 if (iconBounds.height() < mIconHeight / 2) { 82 LauncherInstrumentation.log("hasClickableIcon: icon has insufficient height"); 83 return false; 84 } 85 if (hasSearchBox() && iconCenterInSearchBox(allAppsContainer, icon)) { 86 LauncherInstrumentation.log("hasClickableIcon: icon center is under search box"); 87 return false; 88 } 89 if (iconCenterInRecyclerTopPadding(appListRecycler, icon)) { 90 LauncherInstrumentation.log( 91 "hasClickableIcon: icon center is under the app list recycler's top padding."); 92 return false; 93 } 94 if (iconBounds.bottom > displayBottom) { 95 LauncherInstrumentation.log("hasClickableIcon: icon bottom below bottom offset"); 96 return false; 97 } 98 LauncherInstrumentation.log("hasClickableIcon: icon is clickable"); 99 return true; 100 } 101 iconCenterInSearchBox(UiObject2 allAppsContainer, UiObject2 icon)102 private boolean iconCenterInSearchBox(UiObject2 allAppsContainer, UiObject2 icon) { 103 final Point iconCenter = icon.getVisibleCenter(); 104 return mLauncher.getVisibleBounds(getSearchBox(allAppsContainer)).contains( 105 iconCenter.x, iconCenter.y); 106 } 107 iconCenterInRecyclerTopPadding(UiObject2 appsListRecycler, UiObject2 icon)108 private boolean iconCenterInRecyclerTopPadding(UiObject2 appsListRecycler, UiObject2 icon) { 109 final Point iconCenter = icon.getVisibleCenter(); 110 111 return iconCenter.y <= mLauncher.getVisibleBounds(appsListRecycler).top 112 + getAppsListRecyclerTopPadding(); 113 } 114 115 /** 116 * Finds an icon. If the icon doesn't exist, return null. 117 * Scrolls the app list when needed to make sure the icon is visible. 118 * 119 * @param appName name of the app. 120 * @return The app if found, and null if not found. 121 */ 122 @Nullable tryGetAppIcon(String appName)123 public AppIcon tryGetAppIcon(String appName) { 124 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 125 LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 126 "getting app icon " + appName + " on all apps")) { 127 final UiObject2 allAppsContainer = verifyActiveContainer(); 128 final UiObject2 appListRecycler = getAppListRecycler(allAppsContainer); 129 130 int deviceHeight = mLauncher.getRealDisplaySize().y; 131 int bottomGestureStartOnScreen = mLauncher.getBottomGestureStartOnScreen(); 132 final BySelector appIconSelector = AppIcon.getAppIconSelector(appName, mLauncher); 133 if (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector, 134 bottomGestureStartOnScreen)) { 135 scrollBackToBeginning(); 136 int attempts = 0; 137 int scroll = getAllAppsScroll(); 138 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled")) { 139 while (!hasClickableIcon(allAppsContainer, appListRecycler, appIconSelector, 140 bottomGestureStartOnScreen)) { 141 mLauncher.scrollToLastVisibleRow( 142 allAppsContainer, 143 getBottomVisibleIconBounds(allAppsContainer), 144 mLauncher.getVisibleBounds(appListRecycler).top 145 + getAppsListRecyclerTopPadding() 146 - mLauncher.getVisibleBounds(allAppsContainer).top, 147 getAppsListRecyclerBottomPadding()); 148 verifyActiveContainer(); 149 final int newScroll = getAllAppsScroll(); 150 LauncherInstrumentation.log( 151 String.format("tryGetAppIcon: scrolled from %d to %d", scroll, 152 newScroll)); 153 mLauncher.assertTrue( 154 "Scrolled in a wrong direction in AllApps: from " + scroll + " to " 155 + newScroll, newScroll >= scroll); 156 if (newScroll == scroll) break; 157 158 mLauncher.assertTrue( 159 "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS, 160 ++attempts <= MAX_SCROLL_ATTEMPTS); 161 scroll = newScroll; 162 } 163 } 164 verifyActiveContainer(); 165 } 166 // Ignore bottom offset selection here as there might not be any scroll more scroll 167 // region available. 168 if (hasClickableIcon( 169 allAppsContainer, appListRecycler, appIconSelector, deviceHeight)) { 170 171 final UiObject2 appIcon = mLauncher.waitForObjectInContainer(appListRecycler, 172 appIconSelector); 173 return createAppIcon(appIcon); 174 } else { 175 return null; 176 } 177 } 178 } 179 180 /** @return visible bounds of the top-most visible icon in the container. */ getTopVisibleIconBounds(UiObject2 allAppsContainer)181 protected Rect getTopVisibleIconBounds(UiObject2 allAppsContainer) { 182 return mLauncher.getVisibleBounds(Collections.min(getVisibleIcons(allAppsContainer), 183 Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top))); 184 } 185 186 /** @return visible bounds of the bottom-most visible icon in the container. */ getBottomVisibleIconBounds(UiObject2 allAppsContainer)187 protected Rect getBottomVisibleIconBounds(UiObject2 allAppsContainer) { 188 return mLauncher.getVisibleBounds(Collections.max(getVisibleIcons(allAppsContainer), 189 Comparator.comparingInt(i -> mLauncher.getVisibleBounds(i).top))); 190 } 191 192 @NonNull getVisibleIcons(UiObject2 allAppsContainer)193 private List<UiObject2> getVisibleIcons(UiObject2 allAppsContainer) { 194 return mLauncher.getObjectsInContainer(allAppsContainer, "icon") 195 .stream() 196 .filter(icon -> 197 mLauncher.getVisibleBounds(icon).top 198 < mLauncher.getBottomGestureStartOnScreen()) 199 .collect(Collectors.toList()); 200 } 201 202 /** 203 * Finds an icon. Fails if the icon doesn't exist. Scrolls the app list when needed to make 204 * sure the icon is visible. 205 * 206 * @param appName name of the app. 207 * @return The app. 208 */ 209 @NonNull getAppIcon(String appName)210 public AppIcon getAppIcon(String appName) { 211 AppIcon appIcon = tryGetAppIcon(appName); 212 mLauncher.assertNotNull("Unable to scroll to a clickable icon: " + appName, appIcon); 213 // appIcon.getAppName() checks for content description, so it is possible that it can have 214 // trailing words. So check if the content description contains the appName. 215 mLauncher.assertTrue("Wrong app icon name.", appIcon.getAppName().contains(appName)); 216 return appIcon; 217 } 218 219 @NonNull createAppIcon(UiObject2 icon)220 protected abstract AppIcon createAppIcon(UiObject2 icon); 221 hasSearchBox()222 protected abstract boolean hasSearchBox(); 223 getAppsListRecyclerTopPadding()224 protected abstract int getAppsListRecyclerTopPadding(); 225 getAppsListRecyclerBottomPadding()226 protected int getAppsListRecyclerBottomPadding() { 227 return mLauncher.getTestInfo(TestProtocol.REQUEST_ALL_APPS_BOTTOM_PADDING) 228 .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 229 } 230 scrollBackToBeginning()231 private void scrollBackToBeginning() { 232 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer( 233 "want to scroll back in all apps")) { 234 LauncherInstrumentation.log("Scrolling to the beginning"); 235 final UiObject2 allAppsContainer = verifyActiveContainer(); 236 237 int attempts = 0; 238 final Rect margins = new Rect( 239 /* left= */ 0, 240 getTopVisibleIconBounds(allAppsContainer).bottom, 241 /* right= */ 0, 242 /* bottom= */ getAppsListRecyclerBottomPadding()); 243 244 for (int scroll = getAllAppsScroll(); 245 scroll != 0; 246 scroll = getAllAppsScroll()) { 247 mLauncher.assertTrue("Negative scroll position", scroll > 0); 248 249 mLauncher.assertTrue( 250 "Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS, 251 ++attempts <= MAX_SCROLL_ATTEMPTS); 252 253 mLauncher.scroll( 254 allAppsContainer, 255 Direction.UP, 256 margins, 257 /* steps= */ 12, 258 /* slowDown= */ false); 259 } 260 261 try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer("scrolled up")) { 262 verifyActiveContainer(); 263 } 264 } 265 } 266 getAllAppsScroll()267 protected abstract int getAllAppsScroll(); 268 getAppListRecycler(UiObject2 allAppsContainer)269 protected UiObject2 getAppListRecycler(UiObject2 allAppsContainer) { 270 return mLauncher.waitForObjectInContainer(allAppsContainer, "apps_list_view"); 271 } 272 getSearchBox(UiObject2 allAppsContainer)273 protected UiObject2 getSearchBox(UiObject2 allAppsContainer) { 274 return mLauncher.waitForObjectInContainer(allAppsContainer, "search_container_all_apps"); 275 } 276 277 /** 278 * Flings forward (down) and waits the fling's end. 279 */ flingForward()280 public void flingForward() { 281 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 282 LauncherInstrumentation.Closable c = 283 mLauncher.addContextLayer("want to fling forward in all apps")) { 284 final UiObject2 allAppsContainer = verifyActiveContainer(); 285 // Start the gesture in the center to avoid starting at elements near the top. 286 mLauncher.scroll( 287 allAppsContainer, 288 Direction.DOWN, 289 new Rect(0, 0, 0, mHeight / 2), 290 /* steps= */ 10, 291 /* slowDown= */ false); 292 verifyActiveContainer(); 293 } 294 } 295 296 /** 297 * Flings backward (up) and waits the fling's end. 298 */ flingBackward()299 public void flingBackward() { 300 try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck(); 301 LauncherInstrumentation.Closable c = 302 mLauncher.addContextLayer("want to fling backward in all apps")) { 303 final UiObject2 allAppsContainer = verifyActiveContainer(); 304 // Start the gesture in the center, for symmetry with forward. 305 mLauncher.scroll( 306 allAppsContainer, 307 Direction.UP, 308 new Rect(0, mHeight / 2, 0, 0), 309 /* steps= */ 10, 310 /*slowDown= */ false); 311 verifyActiveContainer(); 312 } 313 } 314 315 /** 316 * Freezes updating app list upon app install/uninstall/update. 317 */ freeze()318 public void freeze() { 319 mLauncher.getTestInfo(TestProtocol.REQUEST_FREEZE_APP_LIST); 320 } 321 322 /** 323 * Resumes updating app list upon app install/uninstall/update. 324 */ unfreeze()325 public void unfreeze() { 326 mLauncher.getTestInfo(TestProtocol.REQUEST_UNFREEZE_APP_LIST); 327 } 328 verifyNotFrozen(String message)329 private void verifyNotFrozen(String message) { 330 mLauncher.assertEquals(message, 0, getFreezeFlags() & DEFER_UPDATES_TEST); 331 mLauncher.assertTrue(message, mLauncher.waitAndGet(() -> getFreezeFlags() == 0, 332 WAIT_TIME_MS, DEFAULT_POLL_INTERVAL)); 333 } 334 getFreezeFlags()335 private int getFreezeFlags() { 336 final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS); 337 return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); 338 } 339 340 /** 341 * Return the QSB UI object on the AllApps screen. 342 * @return the QSB UI object. 343 */ 344 @NonNull getQsb()345 public abstract Qsb getQsb(); 346 }