1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.tapl;
18 
19 import static com.android.launcher3.testing.TestProtocol.ALL_APPS_STATE_ORDINAL;
20 import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL;
21 import static com.android.launcher3.testing.TestProtocol.SPRING_LOADED_STATE_ORDINAL;
22 
23 import static junit.framework.TestCase.assertTrue;
24 
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.os.SystemClock;
28 import android.view.KeyEvent;
29 import android.view.MotionEvent;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.test.uiautomator.By;
34 import androidx.test.uiautomator.Direction;
35 import androidx.test.uiautomator.UiObject2;
36 
37 import com.android.launcher3.testing.TestProtocol;
38 
39 import java.util.List;
40 import java.util.function.Supplier;
41 import java.util.regex.Pattern;
42 import java.util.stream.Collectors;
43 
44 /**
45  * Operations on the workspace screen.
46  */
47 public final class Workspace extends Home {
48     private static final int FLING_STEPS = 10;
49     private static final int DEFAULT_DRAG_STEPS = 10;
50     private static final String DROP_BAR_RES_ID = "drop_target_bar";
51     private static final String DELETE_TARGET_TEXT_ID = "delete_target_text";
52 
53     static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
54             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
55                     + ".*?metaState=META_CTRL_ON");
56     static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
57             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
58                     + ".*?metaState=META_CTRL_ON");
59     private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
60 
61     private final UiObject2 mHotseat;
62 
Workspace(LauncherInstrumentation launcher)63     Workspace(LauncherInstrumentation launcher) {
64         super(launcher);
65         mHotseat = launcher.waitForLauncherObject("hotseat");
66     }
67 
68     /**
69      * Swipes up to All Apps.
70      *
71      * @return the All Apps object.
72      */
73     @NonNull
switchToAllApps()74     public AllApps switchToAllApps() {
75         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
76              LauncherInstrumentation.Closable c =
77                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
78             verifyActiveContainer();
79             final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
80             final int bottomGestureMargin = mLauncher.getBottomGestureSize();
81             final int windowCornerRadius = (int) Math.ceil(mLauncher.getWindowCornerRadius());
82             final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
83             final int swipeHeight = mLauncher.getTestInfo(
84                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
85                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
86             LauncherInstrumentation.log(
87                     "switchToAllApps: deviceHeight = " + deviceHeight + ", startY = " + startY
88                             + ", swipeHeight = " + swipeHeight + ", slop = "
89                             + mLauncher.getTouchSlop());
90 
91             mLauncher.swipeToState(
92                     windowCornerRadius,
93                     startY,
94                     windowCornerRadius,
95                     startY - swipeHeight - mLauncher.getTouchSlop(),
96                     12,
97                     ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
98 
99             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
100                     "swiped to all apps")) {
101                 return new AllApps(mLauncher);
102             }
103         }
104     }
105 
106     /**
107      * Returns an icon for the app, if currently visible.
108      *
109      * @param appName name of the app
110      * @return app icon, if found, null otherwise.
111      */
112     @Nullable
tryGetWorkspaceAppIcon(String appName)113     public AppIcon tryGetWorkspaceAppIcon(String appName) {
114         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
115                 "want to get a workspace icon")) {
116             final UiObject2 workspace = verifyActiveContainer();
117             final UiObject2 icon = workspace.findObject(
118                     AppIcon.getAppIconSelector(appName, mLauncher));
119             return icon != null ? new AppIcon(mLauncher, icon) : null;
120         }
121     }
122 
123 
124     /**
125      * Returns an icon for the app; fails if the icon doesn't exist.
126      *
127      * @param appName name of the app
128      * @return app icon.
129      */
130     @NonNull
getWorkspaceAppIcon(String appName)131     public AppIcon getWorkspaceAppIcon(String appName) {
132         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
133                 "want to get a workspace icon")) {
134             return new AppIcon(mLauncher,
135                     mLauncher.waitForObjectInContainer(
136                             verifyActiveContainer(),
137                             AppIcon.getAppIconSelector(appName, mLauncher)));
138         }
139     }
140 
141     /**
142      * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the
143      * second screen.
144      */
ensureWorkspaceIsScrollable()145     public void ensureWorkspaceIsScrollable() {
146         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
147             final UiObject2 workspace = verifyActiveContainer();
148             if (!isWorkspaceScrollable(workspace)) {
149                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
150                         "dragging icon to a second page of workspace to make it scrollable")) {
151                     dragIcon(workspace, getHotseatAppIcon("Chrome"), pagesPerScreen());
152                     verifyActiveContainer();
153                 }
154             }
155             assertTrue("Home screen workspace didn't become scrollable",
156                     isWorkspaceScrollable(workspace));
157         }
158     }
159 
160     /**
161      * Returns the number of pages that are visible on the screen simultaneously.
162      */
pagesPerScreen()163     public int pagesPerScreen() {
164         return mLauncher.isTwoPanels() ? 2 : 1;
165     }
166 
167     /**
168      * Drags an icon to the (currentPage + pageDelta) page if the page already exists.
169      * If the target page doesn't exist, the icon will be put onto an existing page that is the
170      * closest to the target page.
171      *
172      * @param appIcon   - icon to drag.
173      * @param pageDelta - how many pages should the icon be dragged from the current page.
174      *                    It can be a negative value.
175      */
dragIcon(AppIcon appIcon, int pageDelta)176     public void dragIcon(AppIcon appIcon, int pageDelta) {
177         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
178             final UiObject2 workspace = verifyActiveContainer();
179             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
180                     "dragging icon to page with delta: " + pageDelta)) {
181                 dragIcon(workspace, appIcon, pageDelta);
182                 verifyActiveContainer();
183             }
184         }
185     }
186 
dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta)187     private void dragIcon(UiObject2 workspace, AppIcon appIcon, int pageDelta) {
188         int pageWidth = mLauncher.getDevice().getDisplayWidth() / pagesPerScreen();
189         int targetX = (pageWidth / 2) + pageWidth * pageDelta;
190         dragIconToWorkspace(
191                 mLauncher,
192                 appIcon,
193                 new Point(targetX, mLauncher.getVisibleBounds(workspace).centerY()),
194                 "popup_container",
195                 false,
196                 false,
197                 () -> mLauncher.expectEvent(
198                         TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
199         verifyActiveContainer();
200     }
201 
isWorkspaceScrollable(UiObject2 workspace)202     private boolean isWorkspaceScrollable(UiObject2 workspace) {
203         return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1);
204     }
205 
206     @NonNull
getHotseatAppIcon(String appName)207     public AppIcon getHotseatAppIcon(String appName) {
208         return new AppIcon(mLauncher, mLauncher.waitForObjectInContainer(
209                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
210     }
211 
getStartDragThreshold(LauncherInstrumentation launcher)212     private static int getStartDragThreshold(LauncherInstrumentation launcher) {
213         return launcher.getTestInfo(TestProtocol.REQUEST_START_DRAG_THRESHOLD).getInt(
214                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
215     }
216 
217     /*
218      * Get the center point of the delete icon in the drop target bar.
219      */
getDeleteDropPoint()220     private Point getDeleteDropPoint() {
221         return mLauncher.waitForObjectInContainer(
222                 mLauncher.waitForLauncherObject(DROP_BAR_RES_ID),
223                 DELETE_TARGET_TEXT_ID).getVisibleCenter();
224     }
225 
226     /**
227      * Delete the appIcon from the workspace.
228      *
229      * @param appIcon to be deleted.
230      * @return validated workspace after the existing appIcon being deleted.
231      */
deleteAppIcon(AppIcon appIcon)232     public Workspace deleteAppIcon(AppIcon appIcon) {
233         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
234              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
235                      "removing app icon from workspace")) {
236             dragIconToWorkspace(
237                     mLauncher, appIcon,
238                     () -> getDeleteDropPoint(),
239                     true, /* decelerating */
240                     appIcon.getLongPressIndicator(),
241                     () -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
242                     null);
243 
244             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
245                     "dragged the app to the drop bar")) {
246                 return new Workspace(mLauncher);
247             }
248         }
249     }
250 
251     /**
252      * Finds folder icons in the current workspace.
253      *
254      * @return a list of folder icons.
255      */
getFolderIcons()256     List<FolderIcon> getFolderIcons() {
257         final UiObject2 workspace = verifyActiveContainer();
258         return mLauncher.getObjectsInContainer(workspace, "folder_icon_name").stream().map(
259                 o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList());
260     }
261 
262     /**
263      * Drag an icon up with a short distance that makes workspace go to spring loaded state.
264      *
265      * @return the position after dragging.
266      */
dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime, UiObject2 icon, String longPressIndicator, Runnable expectLongClickEvents)267     private static Point dragIconToSpringLoaded(LauncherInstrumentation launcher, long downTime,
268             UiObject2 icon,
269             String longPressIndicator, Runnable expectLongClickEvents) {
270         final Point iconCenter = icon.getVisibleCenter();
271         final Point dragStartCenter = new Point(iconCenter.x,
272                 iconCenter.y - getStartDragThreshold(launcher));
273 
274         launcher.runToState(() -> {
275             launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
276                     iconCenter, LauncherInstrumentation.GestureScope.INSIDE);
277             LauncherInstrumentation.log("dragIconToSpringLoaded: sent down");
278             expectLongClickEvents.run();
279             launcher.waitForLauncherObject(longPressIndicator);
280             LauncherInstrumentation.log("dragIconToSpringLoaded: indicator");
281             launcher.movePointer(iconCenter, dragStartCenter, DEFAULT_DRAG_STEPS, false,
282                     downTime, true, LauncherInstrumentation.GestureScope.INSIDE);
283         }, SPRING_LOADED_STATE_ORDINAL, "long-pressing and triggering drag start");
284         return dragStartCenter;
285     }
286 
dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime, @Nullable Runnable expectedEvents)287     private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime,
288             @Nullable Runnable expectedEvents) {
289         launcher.runToState(
290                 () -> launcher.sendPointer(
291                         downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
292                         LauncherInstrumentation.GestureScope.INSIDE),
293                 NORMAL_STATE_ORDINAL,
294                 "sending UP event");
295         if (expectedEvents != null) {
296             expectedEvents.run();
297         }
298         LauncherInstrumentation.log("dropIcon: end");
299         launcher.waitUntilLauncherObjectGone("drop_target_bar");
300     }
301 
dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents)302     static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable,
303             Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut,
304             Runnable expectLongClickEvents) {
305         Runnable expectDropEvents = null;
306         if (startsActivity || isWidgetShortcut) {
307             expectDropEvents = () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN,
308                     LauncherInstrumentation.EVENT_START);
309         }
310         dragIconToWorkspace(launcher, launchable, () -> dest, false, longPressIndicator,
311                 expectLongClickEvents, expectDropEvents);
312     }
313 
314     /**
315      * Drag icon in workspace to else where.
316      * This function expects the launchable is inside the workspace and there is no drop event.
317      */
dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> destSupplier, String longPressIndicator)318     static void dragIconToWorkspace(LauncherInstrumentation launcher, Launchable launchable,
319             Supplier<Point> destSupplier, String longPressIndicator) {
320         dragIconToWorkspace(launcher, launchable, destSupplier, false, longPressIndicator,
321                 () -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT), null);
322     }
323 
dragIconToWorkspace( LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> dest, boolean isDecelerating, String longPressIndicator, Runnable expectLongClickEvents, @Nullable Runnable expectDropEvents)324     static void dragIconToWorkspace(
325             LauncherInstrumentation launcher, Launchable launchable, Supplier<Point> dest,
326             boolean isDecelerating, String longPressIndicator, Runnable expectLongClickEvents,
327             @Nullable Runnable expectDropEvents) {
328         try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
329                 "want to drag icon to workspace")) {
330             final long downTime = SystemClock.uptimeMillis();
331             Point dragStart = dragIconToSpringLoaded(launcher, downTime,
332                     launchable.getObject(), longPressIndicator, expectLongClickEvents);
333             Point targetDest = dest.get();
334             int displayX = launcher.getRealDisplaySize().x;
335 
336             // Since the destination can be on another page, we need to drag to the edge first
337             // until we reach the target page
338             while (targetDest.x > displayX || targetDest.x < 0) {
339                 int edgeX = targetDest.x > 0 ? displayX : 0;
340                 Point screenEdge = new Point(edgeX, targetDest.y);
341                 launcher.movePointer(dragStart, screenEdge, DEFAULT_DRAG_STEPS, isDecelerating,
342                         downTime, true, LauncherInstrumentation.GestureScope.INSIDE);
343                 launcher.waitForIdle(); // Wait for the page change to happen
344                 targetDest.x += displayX * (targetDest.x > 0 ? -1 : 1);
345                 dragStart = screenEdge;
346             }
347 
348             // targetDest.x is now between 0 and displayX so we found the target page,
349             // we just have to put move the icon to the destination and drop it
350             launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
351                     downTime, true, LauncherInstrumentation.GestureScope.INSIDE);
352             dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
353         }
354     }
355 
356     /**
357      * Flings to get to screens on the right. Waits for scrolling and a possible overscroll
358      * recoil to complete.
359      */
flingForward()360     public void flingForward() {
361         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
362             final UiObject2 workspace = verifyActiveContainer();
363             mLauncher.scroll(workspace, Direction.RIGHT,
364                     new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
365                     FLING_STEPS, false);
366             verifyActiveContainer();
367         }
368     }
369 
370     /**
371      * Flings to get to screens on the left.  Waits for scrolling and a possible overscroll
372      * recoil to complete.
373      */
flingBackward()374     public void flingBackward() {
375         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
376             final UiObject2 workspace = verifyActiveContainer();
377             mLauncher.scroll(workspace, Direction.LEFT,
378                     new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
379                     FLING_STEPS, false);
380             verifyActiveContainer();
381         }
382     }
383 
384     /**
385      * Opens widgets container by pressing Ctrl+W.
386      *
387      * @return the widgets container.
388      */
389     @NonNull
openAllWidgets()390     public Widgets openAllWidgets() {
391         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
392             verifyActiveContainer();
393             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_DOWN);
394             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP);
395             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
396             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
397                 return new Widgets(mLauncher);
398             }
399         }
400     }
401 
402     @Override
getSwipeHeightRequestName()403     protected String getSwipeHeightRequestName() {
404         return TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT;
405     }
406 
407     @Override
getSwipeStartY()408     protected int getSwipeStartY() {
409         return mLauncher.getRealDisplaySize().y - 1;
410     }
411 
412     @Nullable
tryGetWidget(String label, long timeout)413     public Widget tryGetWidget(String label, long timeout) {
414         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
415                 "getting widget " + label + " on workspace with timeout " + timeout)) {
416             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
417                     By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label),
418                     timeout);
419             return widget != null ? new Widget(mLauncher, widget) : null;
420         }
421     }
422 
423     @Nullable
tryGetPendingWidget(long timeout)424     public Widget tryGetPendingWidget(long timeout) {
425         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
426                 "getting pending widget on workspace with timeout " + timeout)) {
427             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
428                     By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
429             return widget != null ? new Widget(mLauncher, widget) : null;
430         }
431     }
432 }