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