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_META_RIGHT;
20 import static android.view.KeyEvent.KEYCODE_RECENT_APPS;
21 import static android.view.KeyEvent.KEYCODE_TAB;
22 import static android.view.KeyEvent.META_META_ON;
23 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SCROLLED;
24 
25 import static com.android.launcher3.testing.shared.TestProtocol.ALL_APPS_STATE_ORDINAL;
26 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
27 import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
28 
29 import static junit.framework.TestCase.assertNotNull;
30 import static junit.framework.TestCase.assertTrue;
31 
32 import android.graphics.Point;
33 import android.graphics.Rect;
34 import android.os.SystemClock;
35 import android.view.KeyEvent;
36 import android.view.MotionEvent;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 import androidx.test.uiautomator.By;
41 import androidx.test.uiautomator.BySelector;
42 import androidx.test.uiautomator.Direction;
43 import androidx.test.uiautomator.UiDevice;
44 import androidx.test.uiautomator.UiObject2;
45 import androidx.test.uiautomator.Until;
46 
47 import com.android.launcher3.testing.shared.HotseatCellCenterRequest;
48 import com.android.launcher3.testing.shared.TestProtocol;
49 import com.android.launcher3.testing.shared.WorkspaceCellCenterRequest;
50 
51 import java.util.List;
52 import java.util.Map;
53 import java.util.function.Supplier;
54 import java.util.regex.Pattern;
55 import java.util.stream.Collectors;
56 
57 /**
58  * Operations on the workspace screen.
59  */
60 public final class Workspace extends Home {
61     private static final int FLING_STEPS = 10;
62     private static final int DEFAULT_DRAG_STEPS = 10;
63     private static final String DROP_BAR_RES_ID = "drop_target_bar";
64     private static final String DELETE_TARGET_TEXT_ID = "delete_target_text";
65     private static final String UNINSTALL_TARGET_TEXT_ID = "uninstall_target_text";
66     static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
67             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
68                     + ".*?metaState=META_CTRL_ON");
69     static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
70     public static final int MAX_WORKSPACE_DRAG_TRIES = 100;
71 
72     private final UiObject2 mHotseat;
73 
Workspace(LauncherInstrumentation launcher)74     Workspace(LauncherInstrumentation launcher) {
75         super(launcher);
76         mHotseat = launcher.waitForLauncherObject("hotseat");
77     }
78 
79     /**
80      * Swipes up to All Apps.
81      *
82      * @return the All Apps object.
83      */
84     @NonNull
switchToAllApps()85     public HomeAllApps switchToAllApps() {
86         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
87              LauncherInstrumentation.Closable c =
88                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
89             verifyActiveContainer();
90             final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
91             final int bottomGestureMargin = mLauncher.getBottomGestureSize();
92             final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
93             final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
94             final int swipeHeight = mLauncher.getTestInfo(
95                             TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT)
96                     .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
97             LauncherInstrumentation.log(
98                     "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
99                             + ", swipeHeight = " + swipeHeight + ", slop = "
100                             + mLauncher.getTouchSlop());
101 
102             mLauncher.swipeToState(
103                     windowCornerRadius,
104                     startY,
105                     windowCornerRadius,
106                     startY - swipeHeight - mLauncher.getTouchSlop(),
107                     12,
108                     ALL_APPS_STATE_ORDINAL,
109                     LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
110 
111             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
112                     "swiped to all apps")) {
113                 return new HomeAllApps(mLauncher);
114             }
115         }
116     }
117 
118     /** Opens the Launcher all apps page with the meta keyboard shortcut. */
openAllAppsFromKeyboardShortcut()119     public HomeAllApps openAllAppsFromKeyboardShortcut() {
120         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
121              LauncherInstrumentation.Closable c =
122                      mLauncher.addContextLayer("want to open all apps search")) {
123             verifyActiveContainer();
124             mLauncher.runToState(
125                     () -> mLauncher.getDevice().pressKeyCode(KEYCODE_META_RIGHT),
126                     ALL_APPS_STATE_ORDINAL,
127                     "pressing keyboard shortcut");
128             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
129                     "pressed meta key")) {
130                 return new HomeAllApps(mLauncher);
131             }
132         }
133     }
134 
135     /** Opens the Launcher Overview page with the action+tab keyboard shortcut. */
openOverviewFromActionPlusTabKeyboardShortcut()136     public Overview openOverviewFromActionPlusTabKeyboardShortcut() {
137         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
138              LauncherInstrumentation.Closable c =
139                      mLauncher.addContextLayer("want to open overview")) {
140             verifyActiveContainer();
141             mLauncher.runToState(
142                     () -> mLauncher.getDevice().pressKeyCode(KEYCODE_TAB, META_META_ON),
143                     OVERVIEW_STATE_ORDINAL,
144                     "pressing keyboard shortcut");
145             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
146                     "pressed meta+tab key")) {
147                 return new Overview(mLauncher);
148             }
149         }
150     }
151 
152     /** Opens the Launcher Overview page with the Recents keyboard shortcut. */
openOverviewFromRecentsKeyboardShortcut()153     public Overview openOverviewFromRecentsKeyboardShortcut() {
154         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
155              LauncherInstrumentation.Closable c =
156                      mLauncher.addContextLayer("want to open overview")) {
157             verifyActiveContainer();
158             mLauncher.runToState(
159                     () -> mLauncher.getDevice().pressKeyCode(KEYCODE_RECENT_APPS),
160                     OVERVIEW_STATE_ORDINAL,
161                     "pressing keyboard shortcut");
162             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
163                     "pressed recents apps key")) {
164                 return new Overview(mLauncher);
165             }
166         }
167     }
168 
169     /**
170      * Returns the home qsb.
171      *
172      * The qsb must already be visible when calling this method.
173      */
174     @NonNull
getQsb()175     public Qsb getQsb() {
176         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
177                 "want to get the home qsb")) {
178             return new HomeQsb(mLauncher, mHotseat);
179         }
180     }
181 
182     /**
183      * Returns an icon for the app, if currently visible.
184      *
185      * @param appName name of the app
186      * @return app icon, if found, null otherwise.
187      */
188     @Nullable
tryGetWorkspaceAppIcon(String appName)189     public HomeAppIcon tryGetWorkspaceAppIcon(String appName) {
190         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
191                 "want to get a workspace icon")) {
192             final UiObject2 workspace = verifyActiveContainer();
193             final UiObject2 icon = workspace.findObject(
194                     AppIcon.getAppIconSelector(appName, mLauncher));
195             return icon != null ? new WorkspaceAppIcon(mLauncher, icon) : null;
196         }
197     }
198 
199     /**
200      * Waits for an app icon to be gone (e.g. after uninstall). Fails if it remains.
201      *
202      * @param errorMessage error message thrown then the icon doesn't disappear.
203      * @param appName      app that should be gone.
204      */
verifyWorkspaceAppIconIsGone(String errorMessage, String appName)205     public void verifyWorkspaceAppIconIsGone(String errorMessage, String appName) {
206         final UiObject2 workspace = verifyActiveContainer();
207         assertTrue(errorMessage,
208                 workspace.wait(
209                         Until.gone(AppIcon.getAppIconSelector(appName, mLauncher)),
210                         LauncherInstrumentation.WAIT_TIME_MS));
211     }
212 
213 
214     /**
215      * Returns an icon for the app; fails if the icon doesn't exist.
216      *
217      * @param appName name of the app
218      * @return app icon.
219      */
220     @NonNull
getWorkspaceAppIcon(String appName)221     public HomeAppIcon getWorkspaceAppIcon(String appName) {
222         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
223                 "want to get a workspace icon")) {
224             return new WorkspaceAppIcon(mLauncher,
225                     mLauncher.waitForObjectInContainer(
226                             verifyActiveContainer(),
227                             AppIcon.getAppIconSelector(appName, mLauncher)));
228         }
229     }
230 
231     /**
232      * Ensures that workspace is scrollable. If it's not, drags a chrome app icon from hotseat
233      * to the second screen.
234      */
ensureWorkspaceIsScrollable()235     public void ensureWorkspaceIsScrollable() {
236         ensureWorkspaceIsScrollable("Chrome");
237     }
238 
239     /**
240      * Ensures that workspace is scrollable. If it's not, drags an icon of a given app name from
241      * hotseat to the second screen.
242      */
ensureWorkspaceIsScrollable(String appName)243     public void ensureWorkspaceIsScrollable(String appName) {
244         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
245             final UiObject2 workspace = verifyActiveContainer();
246             if (!isWorkspaceScrollable(workspace)) {
247                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
248                         "dragging icon to a second page of workspace to make it scrollable")) {
249                     dragIcon(workspace, getHotseatAppIcon(appName), pagesPerScreen());
250                     verifyActiveContainer();
251                 }
252             }
253             assertTrue("Home screen workspace didn't become scrollable",
254                     isWorkspaceScrollable(workspace));
255         }
256     }
257 
258     /** Returns the number of pages. */
getPageCount()259     public int getPageCount() {
260         final UiObject2 workspace = verifyActiveContainer();
261         return workspace.getChildCount();
262     }
263 
264     /**
265      * Returns the number of pages that are visible on the screen simultaneously.
266      */
pagesPerScreen()267     public int pagesPerScreen() {
268         return mLauncher.isTwoPanels() ? 2 : 1;
269     }
270 
271     /**
272      * Drags an icon to the (currentPage + pageDelta) page.
273      * If the target page doesn't exist yet, a new page will be created.
274      * In case the target page can't be created (e.g. existing pages are 0, 1, current: 0,
275      * pageDelta: 3, the latest page that can be created is 2) the icon will be dragged onto the
276      * page that can be created and is closest to the target page.
277      *
278      * @param homeAppIcon - icon to drag.
279      * @param pageDelta   - how many pages should the icon be dragged from the current page.
280      *                    It can be a negative value. currentPage + pageDelta should be greater
281      *                    than or equal to 0.
282      */
dragIcon(HomeAppIcon homeAppIcon, int pageDelta)283     public void dragIcon(HomeAppIcon homeAppIcon, int pageDelta) {
284         if (mHotseat.getVisibleBounds().height() > mHotseat.getVisibleBounds().width()) {
285             throw new UnsupportedOperationException(
286                     "dragIcon does NOT support dragging when the hotseat is on the side.");
287         }
288         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
289             final UiObject2 workspace = verifyActiveContainer();
290             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
291                     "dragging icon to page with delta: " + pageDelta)) {
292                 dragIcon(workspace, homeAppIcon, pageDelta);
293                 verifyActiveContainer();
294             }
295         }
296     }
297 
dragIcon(UiObject2 workspace, HomeAppIcon homeAppIcon, int pageDelta)298     private void dragIcon(UiObject2 workspace, HomeAppIcon homeAppIcon, int pageDelta) {
299         int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen();
300         int targetX = (pageWidth / 2) + pageWidth * pageDelta;
301         int targetY = mLauncher.getVisibleBounds(workspace).centerY();
302         dragIconToWorkspace(
303                 mLauncher,
304                 homeAppIcon,
305                 () -> new Point(targetX, targetY),
306                 false,
307                 false,
308                 () -> mLauncher.expectEvent(
309                         TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
310         verifyActiveContainer();
311     }
312 
isWorkspaceScrollable(UiObject2 workspace)313     private boolean isWorkspaceScrollable(UiObject2 workspace) {
314         return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1);
315     }
316 
317     @NonNull
getHotseatAppIcon(String appName)318     public HomeAppIcon getHotseatAppIcon(String appName) {
319         return new WorkspaceAppIcon(mLauncher, mLauncher.waitForObjectInContainer(
320                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
321     }
322 
323     /**
324      * Returns an icon for the given cell; fails if the icon doesn't exist.
325      *
326      * @param cellInd zero based index number of the hotseat cells.
327      * @return app icon.
328      */
329     @NonNull
getHotseatAppIcon(int cellInd)330     public HomeAppIcon getHotseatAppIcon(int cellInd) {
331         List<UiObject2> icons = mHotseat.findObjects(AppIcon.getAnyAppIconSelector());
332         final Point center = getHotseatCellCenter(mLauncher, cellInd);
333         return icons.stream()
334                 .filter(icon -> icon.getVisibleBounds().contains(center.x, center.y))
335                 .findFirst()
336                 .map(icon -> new WorkspaceAppIcon(mLauncher, icon))
337                 .orElseThrow(() ->
338                         new AssertionError("Unable to get a hotseat icon on " + cellInd));
339     }
340 
341     /**
342      * @return map of text -> center of the view. In case of icons with the same name, the one with
343      * lower x coordinate is selected.
344      */
getAllWorkspaceIconsPositions()345     public Map<String, Point> getAllWorkspaceIconsPositions() {
346         final UiObject2 workspace = verifyActiveContainer();
347         List<UiObject2> workspaceIcons =
348                 mLauncher.waitForObjectsInContainer(workspace, AppIcon.getAnyAppIconSelector());
349         return getIconPositionMap(workspaceIcons);
350     }
351 
352     /**
353      * @return point where icon is found for given the app name,
354      * point is visible center of the icon.
355      */
356     @NonNull
getWorkspaceIconPosition(String appName)357     public Point getWorkspaceIconPosition(String appName) {
358         final UiObject2 workspace = verifyActiveContainer();
359 
360         UiObject2 workspaceIcon =
361                 mLauncher.waitForObjectInContainer(workspace,
362                         AppIcon.getAppIconSelector(appName, mLauncher));
363         return workspaceIcon.getVisibleCenter();
364     }
365 
getIconPositionMap(List<UiObject2> icons)366     private Map<String, Point> getIconPositionMap(List<UiObject2> icons) {
367         return icons.stream()
368                 .collect(
369                         Collectors.toMap(
370                                 /* keyMapper= */ uiObject21 -> {
371                                     return uiObject21.getText();
372                                 },
373                                 /* valueMapper= */ uiObject2 -> {
374                                     return uiObject2.getVisibleCenter();
375                                 },
376                                 /* mergeFunction= */ (p1, p2) -> p1.x < p2.x ? p1 : p2));
377     }
378 
379     /*
380      * Get the center point of the delete/uninstall icon in the drop target bar.
381      */
382     private static Point getDropPointFromDropTargetBar(
383             LauncherInstrumentation launcher, String targetId) {
384         return launcher.waitForObjectInContainer(
385                 launcher.waitForLauncherObject(DROP_BAR_RES_ID),
386                 targetId).getVisibleCenter();
387     }
388 
389     /**
390      * Drag the appIcon from the workspace and cancel by dragging icon to corner of screen where no
391      * drop point exists.
392      *
393      * @param homeAppIcon to be dragged.
394      */
395     @NonNull
396     public Workspace dragAndCancelAppIcon(HomeAppIcon homeAppIcon) {
397         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
398              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
399                      "dragging app icon across workspace")) {
400             dragIconToWorkspace(
401                     mLauncher,
402                     homeAppIcon,
403                     () -> new Point(0, 0),
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT)404                     () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
405                     null,
406                     /* startsActivity = */ false);
407 
try(LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer( "dragged the app across workspace"))408             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
409                     "dragged the app across workspace")) {
410                 return new Workspace(mLauncher);
411             }
412         }
413     }
414 
415     /**
416      * Delete the appIcon from the workspace.
417      *
418      * @param homeAppIcon to be deleted.
419      * @return validated workspace after the existing appIcon being deleted.
420      */
421     public Workspace deleteAppIcon(HomeAppIcon homeAppIcon) {
422         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
423              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
424                      "removing app icon from workspace")) {
425             dragIconToWorkspace(
426                     mLauncher,
427                     homeAppIcon,
428                     () -> getDropPointFromDropTargetBar(mLauncher, DELETE_TARGET_TEXT_ID),
429                     () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
430                     /* expectDropEvents= */ null,
431                     /* startsActivity = */ false);
432 
433             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
434                     "dragged the app to the drop bar")) {
435                 return new Workspace(mLauncher);
436             }
437         }
438     }
439 
440     /**
441      * Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar.
442      *
443      * @param launcher              the root TAPL instrumentation object of {@link
444      *                              LauncherInstrumentation} type.
445      * @param homeAppIcon           to be uninstalled.
446      * @param launcher              the root TAPL instrumentation object of {@link
447      *                              LauncherInstrumentation} type.
448      * @param homeAppIcon           to be uninstalled.
449      * @param expectLongClickEvents the runnable to be executed to verify expected longclick event.
450      * @return validated workspace after the existing appIcon being uninstalled.
451      */
452     static Workspace uninstallAppIcon(LauncherInstrumentation launcher, HomeAppIcon homeAppIcon,
453             Runnable expectLongClickEvents) {
454         try (LauncherInstrumentation.Closable c = launcher.addContextLayer(
455                 "uninstalling app icon")) {
456 
457             final String appNameToUninstall = homeAppIcon.getAppName();
458             dragIconToWorkspace(
459                     launcher,
460                     homeAppIcon,
461                     () -> getDropPointFromDropTargetBar(launcher, UNINSTALL_TARGET_TEXT_ID),
462                     expectLongClickEvents,
463                     /* expectDropEvents= */null,
464                     /* startsActivity = */ false);
465 
466             launcher.waitUntilLauncherObjectGone(DROP_BAR_RES_ID);
467 
468             final BySelector installerAlert = By.text(Pattern.compile(
469                     "Do you want to uninstall this app\\?",
470                     Pattern.DOTALL | Pattern.MULTILINE));
471             final UiDevice device = launcher.getDevice();
472             assertTrue("uninstall alert is not shown", device.wait(
473                     Until.hasObject(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
474             final UiObject2 ok = device.findObject(By.text("OK"));
475             assertNotNull("OK button is not shown", ok);
476             launcher.clickObject(ok);
477             assertTrue("Uninstall alert is not dismissed after clicking OK", device.wait(
478                     Until.gone(installerAlert), LauncherInstrumentation.WAIT_TIME_MS));
479 
480             try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
481                     "uninstalled app by dragging to the drop bar")) {
482                 final Workspace newWorkspace = new Workspace(launcher);
483                 launcher.waitUntilLauncherObjectGone(
484                         AppIcon.getAppIconSelector(appNameToUninstall));
485                 return newWorkspace;
486             }
487         }
488     }
489 
490     /**
491      * Get cell layout's grids size. The return point's x and y values are the cell counts in X and
492      * Y directions respectively, not the values in pixels.
493      */
494     public Point getIconGridDimensions() {
495         int[] countXY = mLauncher.getTestInfo(
496                 TestProtocol.REQUEST_WORKSPACE_CELL_LAYOUT_SIZE).getIntArray(
497                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
498         return new Point(countXY[0], countXY[1]);
499     }
500 
501     static Point getCellCenter(LauncherInstrumentation launcher, int cellX, int cellY) {
502         return launcher.getTestInfo(WorkspaceCellCenterRequest.builder().setCellX(cellX).setCellY(
503                 cellY).build()).getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
504     }
505 
506     static Point getCellCenter(LauncherInstrumentation launcher, int cellX, int cellY, int spanX,
507             int spanY) {
508         return launcher.getTestInfo(WorkspaceCellCenterRequest.builder().setCellX(cellX)
509                 .setCellY(cellY).setSpanX(spanX).setSpanY(spanY).build())
510                 .getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
511     }
512 
513     static Point getHotseatCellCenter(LauncherInstrumentation launcher, int cellInd) {
514         return launcher.getTestInfo(HotseatCellCenterRequest.builder()
515                 .setCellInd(cellInd).build()).getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
516     }
517 
518     /** Returns the number of rows and columns in the workspace */
519     public Point getRowsAndCols() {
520         return mLauncher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_COLUMNS_ROWS).getParcelable(
521                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
522     }
523 
524     /** Returns the index of the current page */
525     public int getCurrentPage() {
526         return getCurrentPage(mLauncher);
527     }
528 
529     /** Returns the index of the current page */
530     private static int getCurrentPage(LauncherInstrumentation launcher) {
531         return launcher.getTestInfo(TestProtocol.REQUEST_WORKSPACE_CURRENT_PAGE_INDEX).getInt(
532                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
533     }
534 
535     /**
536      * Finds folder icons in the current workspace.
537      *
538      * @return a list of folder icons.
539      */
540     List<FolderIcon> getFolderIcons() {
541         final UiObject2 workspace = verifyActiveContainer();
542         return mLauncher.getObjectsInContainer(workspace, "folder_icon_name").stream().map(
543                 o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList());
544     }
545 
546     private static void sendUp(LauncherInstrumentation launcher, Point dest,
547             long downTime) {
548         launcher.sendPointer(
549                 downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
550                 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
551     }
552 
553     private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime,
554             @Nullable Runnable expectedEvents, boolean startsActivity) {
555         if (startsActivity) {
556             launcher.executeAndWaitForLauncherStop(
557                     () -> sendUp(launcher, dest, downTime),
558                     "sending UP event");
559         } else {
560             launcher.runToState(
561                     () -> sendUp(launcher, dest, downTime),
562                     NORMAL_STATE_ORDINAL,
563                     "sending UP event");
564         }
565         if (expectedEvents != null) {
566             expectedEvents.run();
567         }
568         LauncherInstrumentation.log("dropIcon: end");
569         launcher.waitUntilLauncherObjectGone("drop_target_bar");
570     }
571 
572     static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable,
573             Supplier<Point> dest, boolean startsActivity, boolean isWidgetShortcut,
574             Runnable expectLongClickEvents) {
575         Runnable expectDropEvents = null;
576         if (startsActivity || isWidgetShortcut) {
577             expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN,
578                     LauncherInstrumentation.EVENT_START);
579         }
580         dragIconToWorkspace(
581                 launcher, launchable, dest, expectLongClickEvents, expectDropEvents,
582                 startsActivity);
583     }
584 
585     static void dragIconToWorkspaceCellPosition(LauncherInstrumentation launcher,
586             Launchable launchable, int cellX, int cellY, int spanX, int spanY,
587             boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents) {
588         Runnable expectDropEvents = null;
589         if (startsActivity || isWidgetShortcut) {
590             expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN,
591                     LauncherInstrumentation.EVENT_START);
592         }
593         dragIconToWorkspaceCellPosition(
594                 launcher, launchable, cellX, cellY, spanX, spanY, true, expectLongClickEvents,
595                 expectDropEvents);
596     }
597 
598     /**
599      * Drag icon in workspace to else where and drop it immediately.
600      * (There is no slow down time before drop event)
601      * This function expects the launchable is inside the workspace and there is no drop event.
602      */
603     static void dragIconToWorkspace(
604             LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> destSupplier,
605             boolean isDraggingToFolder) {
606         dragIconToWorkspace(
607                 launcher,
608                 launchable,
609                 destSupplier,
610                 /* isDecelerating= */ false,
611                 () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
612                 /* expectDropEvents= */ null,
613                 /* startsActivity = */ false,
614                 isDraggingToFolder);
615     }
616 
617     static void dragIconToWorkspace(
618             LauncherInstrumentation launcher,
619             Launchable launchable,
620             Supplier<Point> dest,
621             Runnable expectLongClickEvents,
622             @Nullable Runnable expectDropEvents,
623             boolean startsActivity) {
624         dragIconToWorkspace(launcher, launchable, dest, /* isDecelerating */ true,
625                 expectLongClickEvents, expectDropEvents, startsActivity,
626                 /* isDraggingToFolder */ false);
627     }
628 
629     static void dragIconToWorkspace(
630             LauncherInstrumentation launcher,
631             Launchable launchable,
632             Supplier<Point> dest,
633             boolean isDecelerating,
634             Runnable expectLongClickEvents,
635             @Nullable Runnable expectDropEvents,
636             boolean startsActivity,
637             boolean isDraggingToFolder) {
638         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
639                 "want to drag icon to workspace")) {
640             final long downTime = SystemClock.uptimeMillis();
641             Point dragStart = launchable.startDrag(
642                     downTime,
643                     expectLongClickEvents,
644                     /* runToSpringLoadedState= */ true);
645             Point targetDest = dest.get();
646             int displayX = launcher.getRealDisplaySize().x;
647 
648             // Since the destination can be on another page, we need to drag to the edge first
649             // until we reach the target page
650             while (targetDest.x > displayX || targetDest.x < 0) {
651                 // Don't drag all the way to the edge to prevent touch events from getting out of
652                 //screen bounds.
653                 int edgeX = targetDest.x > 0 ? displayX - 1 : 1;
654                 Point screenEdge = new Point(edgeX, targetDest.y);
655                 Point finalDragStart = dragStart;
656                 executeAndWaitForPageScroll(launcher,
657                         () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS,
658                                 true, downTime, downTime, true,
659                                 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
660                 targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1);
661                 dragStart = screenEdge;
662             }
663 
664             // targetDest.x is now between 0 and displayX so we found the target page.
665             // If not a folder, we just have to put move the icon to the destination and drop it.
666             // If it's a folder we want to drag to the folder icon and then drag to the center of
667             // that folder when it opens.
668             if (isDraggingToFolder) {
669                 Point finalDragStart = dragStart;
670                 Point finalTargetDest = targetDest;
671                 Folder folder = executeAndWaitForFolderOpen(launcher, () -> launcher.movePointer(
672                         finalDragStart, finalTargetDest, DEFAULT_DRAG_STEPS, isDecelerating,
673                         downTime, SystemClock.uptimeMillis(), false,
674                         LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
675 
676                 Rect dropBounds = folder.getDropLocationBounds();
677                 dragStart = targetDest;
678                 targetDest = new Point(dropBounds.centerX(), dropBounds.centerY());
679             }
680 
681             launcher.movePointer(dragStart, targetDest,
682                     DEFAULT_DRAG_STEPS, isDecelerating, downTime, SystemClock.uptimeMillis(),
683                     false, LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
684 
685             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, startsActivity);
686         }
687     }
688 
689     static void dragIconToWorkspaceCellPosition(
690             LauncherInstrumentation launcher,
691             Launchable launchable,
692             int cellX, int cellY, int spanX, int spanY,
693             boolean isDecelerating,
694             Runnable expectLongClickEvents,
695             @Nullable Runnable expectDropEvents) {
696         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
697                 "want to drag icon to workspace")) {
698             Point rowsAndCols = launcher.getWorkspace().getRowsAndCols();
699             int destinationWorkspace = cellX / rowsAndCols.x;
700             cellX = cellX % rowsAndCols.x;
701 
702             final long downTime = SystemClock.uptimeMillis();
703             Point dragStart = launchable.startDrag(
704                     downTime,
705                     expectLongClickEvents,
706                     /* runToSpringLoadedState= */ true);
707             Point targetDest = getCellCenter(launcher, cellX, cellY, spanX, spanY);
708             // Since the destination can be on another page, we need to drag to the edge first
709             // until we reach the target page
710             dragStart = dragToGivenWorkspace(launcher, dragStart, destinationWorkspace,
711                     targetDest.y);
712 
713             // targetDest.x is now between 0 and displayX so we found the target page,
714             // we just have to put move the icon to the destination and drop it
715             launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
716                     downTime, SystemClock.uptimeMillis(), false,
717                     LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
718             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents,
719                     /* startsActivity = */ false);
720         }
721     }
722 
723     /**
724      * Given a drag that already started at currentPosition, drag the item to the given destination
725      * index defined by destinationWorkspaceIndex.
726      *
727      * @param launcher
728      * @param currentPosition
729      * @param destinationWorkspaceIndex
730      * @param y
731      * @return the finishing position of the drag.
732      */
733     private static Point dragToGivenWorkspace(LauncherInstrumentation launcher,
734             Point currentPosition, int destinationWorkspaceIndex, int y) {
735         final long downTime = SystemClock.uptimeMillis();
736         int displayX = launcher.getRealDisplaySize().x;
737         int currentPage = Workspace.getCurrentPage(launcher);
738         int counter = 0;
739         while (currentPage != destinationWorkspaceIndex) {
740             counter++;
741             if (counter > MAX_WORKSPACE_DRAG_TRIES) {
742                 throw new RuntimeException(
743                         "Wrong destination workspace index " + destinationWorkspaceIndex
744                                 + ", desired workspace was never reached");
745             }
746             // if the destination is greater than current page, set the display edge to be the
747             // right edge. Don't drag all the way to the edge to prevent touch events from
748             // getting out of screen bounds.
749             int displayEdge = destinationWorkspaceIndex > currentPage ? displayX - 1 : 1;
750             Point screenEdge = new Point(displayEdge, y);
751             Point finalDragStart = currentPosition;
752             executeAndWaitForPageScroll(launcher,
753                     () -> launcher.movePointer(finalDragStart, screenEdge, DEFAULT_DRAG_STEPS,
754                             true, downTime, downTime, true,
755                             LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER));
756             currentPage = Workspace.getCurrentPage(launcher);
757             currentPosition = screenEdge;
758         }
759         return currentPosition;
760     }
761 
762     private static void executeAndWaitForPageScroll(LauncherInstrumentation launcher,
763             Runnable command) {
764         launcher.executeAndWaitForEvent(command,
765                 event -> event.getEventType() == TYPE_VIEW_SCROLLED,
766                 () -> "Page scroll didn't happen", "Scrolling page");
767     }
768 
769     private static Folder executeAndWaitForFolderOpen(LauncherInstrumentation launcher,
770             Runnable command) {
771         launcher.executeAndWaitForEvent(command,
772                 event -> TestProtocol.FOLDER_OPENED_MESSAGE.equals(
773                         event.getClassName().toString()),
774                 () -> "Fail to open folder.",
775                 "open folder");
776         return new Folder(launcher);
777     }
778 
779     static void dragIconToHotseat(
780             LauncherInstrumentation launcher,
781             Launchable launchable,
782             Supplier<Point> dest,
783             Runnable expectLongClickEvents,
784             @Nullable Runnable expectDropEvents) {
785         final long downTime = SystemClock.uptimeMillis();
786         Point dragStart = launchable.startDrag(
787                 downTime,
788                 expectLongClickEvents,
789                 /* runToSpringLoadedState= */ true);
790         Point targetDest = dest.get();
791 
792         launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, true,
793                 downTime, SystemClock.uptimeMillis(), false,
794                 LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
795         dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents,
796                 /* startsActivity = */ false);
797     }
798 
799     /**
800      * Flings to get to screens on the right. Waits for scrolling and a possible overscroll
801      * recoil to complete.
802      */
803     public void flingForward() {
804         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
805             Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer());
806             mLauncher.pointerScroll(
807                     workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.RIGHT);
808             verifyActiveContainer();
809         }
810     }
811 
812     /**
813      * Flings to get to screens on the left.  Waits for scrolling and a possible overscroll
814      * recoil to complete.
815      */
816     public void flingBackward() {
817         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
818             Rect workspaceBounds = mLauncher.getVisibleBounds(verifyActiveContainer());
819             mLauncher.pointerScroll(
820                     workspaceBounds.centerX(), workspaceBounds.centerY(), Direction.LEFT);
821             verifyActiveContainer();
822         }
823     }
824 
825     /**
826      * Opens widgets container by pressing Ctrl+W.
827      *
828      * @return the widgets container.
829      */
830     @NonNull
831     public Widgets openAllWidgets() {
832         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
833             verifyActiveContainer();
834             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP);
835             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
836             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
837                 return new Widgets(mLauncher);
838             }
839         }
840     }
841 
842     @Override
843     protected String getSwipeHeightRequestName() {
844         return TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT;
845     }
846 
847     @Override
848     protected int getSwipeStartY() {
849         return mLauncher.getRealDisplaySize().y - 1;
850     }
851 
852     @Nullable
853     public Widget tryGetWidget(String label, long timeout) {
854         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
855                 "getting widget " + label + " on workspace with timeout " + timeout)) {
856             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
857                     By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label),
858                     timeout);
859             return widget != null ? new Widget(mLauncher, widget) : null;
860         }
861     }
862 
863     /**
864      * @param cellX X position of the widget trying to get.
865      * @param cellY Y position of the widget trying to get.
866      * @return returns the Widget in the given position in the Launcher or an Exception if no such
867      * widget is in that position.
868      */
869     @NonNull
870     public Widget getWidgetAtCell(int cellX, int cellY) {
871         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
872                 "getting widget at cell position " + cellX + "," + cellY)) {
873             final List<UiObject2> widgets = mLauncher.waitForObjectsBySelector(
874                     By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView"));
875             Point coordinateInScreen = Workspace.getCellCenter(mLauncher, cellX, cellY);
876             for (UiObject2 widget : widgets) {
877                 if (widget.getVisibleBounds().contains(coordinateInScreen.x,
878                         coordinateInScreen.y)) {
879                     return new Widget(mLauncher, widget);
880                 }
881             }
882         }
883         mLauncher.fail("Unable to find widget at cell " + cellX + "," + cellY);
884         // This statement is unreachable because mLauncher.fail throws an exception
885         // but is needed for compiling
886         return null;
887     }
888 
889     @Nullable
890     public Widget tryGetPendingWidget(long timeout) {
891         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
892                 "getting pending widget on workspace with timeout " + timeout)) {
893             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
894                     By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
895             return widget != null ? new Widget(mLauncher, widget) : null;
896         }
897     }
898 }
899