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