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 }