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.content.res.Resources;
26 import android.graphics.Point;
27 import android.graphics.Rect;
28 import android.os.SystemClock;
29 import android.view.KeyEvent;
30 import android.view.MotionEvent;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.test.uiautomator.By;
35 import androidx.test.uiautomator.Direction;
36 import androidx.test.uiautomator.UiObject2;
37 
38 import com.android.launcher3.ResourceUtils;
39 import com.android.launcher3.testing.TestProtocol;
40 
41 import java.util.regex.Pattern;
42 
43 /**
44  * Operations on the workspace screen.
45  */
46 public final class Workspace extends Home {
47     private static final int FLING_STEPS = 10;
48 
49     static final Pattern EVENT_CTRL_W_DOWN = Pattern.compile(
50             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_W"
51                     + ".*?metaState=META_CTRL_ON");
52     static final Pattern EVENT_CTRL_W_UP = Pattern.compile(
53             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_W"
54                     + ".*?metaState=META_CTRL_ON");
55     private static final Pattern LONG_CLICK_EVENT = Pattern.compile("onWorkspaceItemLongClick");
56 
57     private final UiObject2 mHotseat;
58 
Workspace(LauncherInstrumentation launcher)59     Workspace(LauncherInstrumentation launcher) {
60         super(launcher);
61         mHotseat = launcher.waitForLauncherObject("hotseat");
62     }
63 
supportsRoundedCornersOnWindows(Resources resources)64     private static boolean supportsRoundedCornersOnWindows(Resources resources) {
65         return ResourceUtils.getBoolByName(
66                 "config_supportsRoundedCornersOnWindows", resources, false);
67     }
68 
getWindowCornerRadius(Resources resources)69     private static float getWindowCornerRadius(Resources resources) {
70         if (!supportsRoundedCornersOnWindows(resources)) {
71             return 0f;
72         }
73 
74         // Radius that should be used in case top or bottom aren't defined.
75         float defaultRadius = ResourceUtils.getDimenByName("rounded_corner_radius", resources, 0);
76 
77         float topRadius = ResourceUtils.getDimenByName("rounded_corner_radius_top", resources, 0);
78         if (topRadius == 0f) {
79             topRadius = defaultRadius;
80         }
81         float bottomRadius = ResourceUtils.getDimenByName(
82                 "rounded_corner_radius_bottom", resources, 0);
83         if (bottomRadius == 0f) {
84             bottomRadius = defaultRadius;
85         }
86 
87         // Always use the smallest radius to make sure the rounded corners will
88         // completely cover the display.
89         return Math.min(topRadius, bottomRadius);
90     }
91 
92     /**
93      * Swipes up to All Apps.
94      *
95      * @return the App Apps object.
96      */
97     @NonNull
switchToAllApps()98     public AllApps switchToAllApps() {
99         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
100              LauncherInstrumentation.Closable c =
101                      mLauncher.addContextLayer("want to switch from workspace to all apps")) {
102             verifyActiveContainer();
103             final int deviceHeight = mLauncher.getDevice().getDisplayHeight();
104             final int bottomGestureMargin = ResourceUtils.getNavbarSize(
105                     ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources());
106             final int windowCornerRadius = (int) Math.ceil(getWindowCornerRadius(
107                     mLauncher.getResources()));
108             final int startY = deviceHeight - Math.max(bottomGestureMargin, windowCornerRadius) - 1;
109             final int swipeHeight = mLauncher.getTestInfo(
110                     TestProtocol.REQUEST_HOME_TO_ALL_APPS_SWIPE_HEIGHT).
111                     getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
112             LauncherInstrumentation.log(
113                     "switchToAllApps: swipeHeight = " + swipeHeight + ", slop = "
114                             + mLauncher.getTouchSlop());
115 
116             mLauncher.swipeToState(
117                     0,
118                     startY,
119                     0,
120                     startY - swipeHeight - mLauncher.getTouchSlop(),
121                     12,
122                     ALL_APPS_STATE_ORDINAL, LauncherInstrumentation.GestureScope.INSIDE);
123 
124             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
125                     "swiped to all apps")) {
126                 return new AllApps(mLauncher);
127             }
128         }
129     }
130 
131     /**
132      * Returns an icon for the app, if currently visible.
133      *
134      * @param appName name of the app
135      * @return app icon, if found, null otherwise.
136      */
137     @Nullable
tryGetWorkspaceAppIcon(String appName)138     public AppIcon tryGetWorkspaceAppIcon(String appName) {
139         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
140                 "want to get a workspace icon")) {
141             final UiObject2 workspace = verifyActiveContainer();
142             final UiObject2 icon = workspace.findObject(
143                     AppIcon.getAppIconSelector(appName, mLauncher));
144             return icon != null ? new AppIcon(mLauncher, icon) : null;
145         }
146     }
147 
148 
149     /**
150      * Returns an icon for the app; fails if the icon doesn't exist.
151      *
152      * @param appName name of the app
153      * @return app icon.
154      */
155     @NonNull
getWorkspaceAppIcon(String appName)156     public AppIcon getWorkspaceAppIcon(String appName) {
157         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
158                 "want to get a workspace icon")) {
159             return new AppIcon(mLauncher,
160                     mLauncher.waitForObjectInContainer(
161                             verifyActiveContainer(),
162                             AppIcon.getAppIconSelector(appName, mLauncher)));
163         }
164     }
165 
166     /**
167      * Ensures that workspace is scrollable. If it's not, drags an icon icons from hotseat to the
168      * second screen.
169      */
ensureWorkspaceIsScrollable()170     public void ensureWorkspaceIsScrollable() {
171         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
172             final UiObject2 workspace = verifyActiveContainer();
173             if (!isWorkspaceScrollable(workspace)) {
174                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
175                         "dragging icon to a second page of workspace to make it scrollable")) {
176                     dragIconToWorkspace(
177                             mLauncher,
178                             getHotseatAppIcon("Chrome"),
179                             new Point(mLauncher.getDevice().getDisplayWidth(),
180                                     mLauncher.getVisibleBounds(workspace).centerY()),
181                             "deep_shortcuts_container",
182                             false,
183                             false,
184                             () -> mLauncher.expectEvent(
185                                     TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT));
186                     verifyActiveContainer();
187                 }
188             }
189             assertTrue("Home screen workspace didn't become scrollable",
190                     isWorkspaceScrollable(workspace));
191         }
192     }
193 
isWorkspaceScrollable(UiObject2 workspace)194     private boolean isWorkspaceScrollable(UiObject2 workspace) {
195         return workspace.getChildCount() > 1;
196     }
197 
198     @NonNull
getHotseatAppIcon(String appName)199     public AppIcon getHotseatAppIcon(String appName) {
200         return new AppIcon(mLauncher, mLauncher.waitForObjectInContainer(
201                 mHotseat, AppIcon.getAppIconSelector(appName, mLauncher)));
202     }
203 
dragIconToWorkspace( LauncherInstrumentation launcher, Launchable launchable, Point dest, String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut, Runnable expectLongClickEvents)204     static void dragIconToWorkspace(
205             LauncherInstrumentation launcher, Launchable launchable, Point dest,
206             String longPressIndicator, boolean startsActivity, boolean isWidgetShortcut,
207             Runnable expectLongClickEvents) {
208         LauncherInstrumentation.log("dragIconToWorkspace: begin");
209         final Point launchableCenter = launchable.getObject().getVisibleCenter();
210         final long downTime = SystemClock.uptimeMillis();
211         launcher.runToState(
212                 () -> {
213                     launcher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN,
214                             launchableCenter, LauncherInstrumentation.GestureScope.INSIDE);
215                     LauncherInstrumentation.log("dragIconToWorkspace: sent down");
216                     expectLongClickEvents.run();
217                     launcher.waitForLauncherObject(longPressIndicator);
218                     LauncherInstrumentation.log("dragIconToWorkspace: indicator");
219                     launcher.movePointer(launchableCenter, dest, 10, downTime, true,
220                             LauncherInstrumentation.GestureScope.INSIDE);
221                 },
222                 SPRING_LOADED_STATE_ORDINAL);
223         LauncherInstrumentation.log("dragIconToWorkspace: moved pointer");
224         launcher.runToState(
225                 () -> launcher.sendPointer(
226                         downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
227                         LauncherInstrumentation.GestureScope.INSIDE),
228                 NORMAL_STATE_ORDINAL);
229         if (startsActivity || isWidgetShortcut) {
230             launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LauncherInstrumentation.EVENT_START);
231         }
232         LauncherInstrumentation.log("dragIconToWorkspace: end");
233         launcher.waitUntilLauncherObjectGone("drop_target_bar");
234     }
235 
236     /**
237      * Flings to get to screens on the right. Waits for scrolling and a possible overscroll
238      * recoil to complete.
239      */
flingForward()240     public void flingForward() {
241         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
242             final UiObject2 workspace = verifyActiveContainer();
243             mLauncher.scroll(workspace, Direction.RIGHT,
244                     new Rect(0, 0, mLauncher.getEdgeSensitivityWidth() + 1, 0),
245                     FLING_STEPS, false);
246             verifyActiveContainer();
247         }
248     }
249 
250     /**
251      * Flings to get to screens on the left.  Waits for scrolling and a possible overscroll
252      * recoil to complete.
253      */
flingBackward()254     public void flingBackward() {
255         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
256             final UiObject2 workspace = verifyActiveContainer();
257             mLauncher.scroll(workspace, Direction.LEFT,
258                     new Rect(mLauncher.getEdgeSensitivityWidth() + 1, 0, 0, 0),
259                     FLING_STEPS, false);
260             verifyActiveContainer();
261         }
262     }
263 
264     /**
265      * Opens widgets container by pressing Ctrl+W.
266      *
267      * @return the widgets container.
268      */
269     @NonNull
openAllWidgets()270     public Widgets openAllWidgets() {
271         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
272             verifyActiveContainer();
273             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_DOWN);
274             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_CTRL_W_UP);
275             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
276             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer("pressed Ctrl+W")) {
277                 return new Widgets(mLauncher);
278             }
279         }
280     }
281 
282     @Override
getSwipeHeightRequestName()283     protected String getSwipeHeightRequestName() {
284         return TestProtocol.REQUEST_HOME_TO_OVERVIEW_SWIPE_HEIGHT;
285     }
286 
287     @Override
getSwipeStartY()288     protected int getSwipeStartY() {
289         return mLauncher.getRealDisplaySize().y - 1;
290     }
291 
292     @Nullable
tryGetWidget(String label, long timeout)293     public Widget tryGetWidget(String label, long timeout) {
294         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
295                 "getting widget " + label + " on workspace with timeout " + timeout)) {
296             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
297                     By.clazz("com.android.launcher3.widget.LauncherAppWidgetHostView").desc(label),
298                     timeout);
299             return widget != null ? new Widget(mLauncher, widget) : null;
300         }
301     }
302 
303     @Nullable
tryGetPendingWidget(long timeout)304     public Widget tryGetPendingWidget(long timeout) {
305         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
306                 "getting pending widget on workspace with timeout " + timeout)) {
307             final UiObject2 widget = mLauncher.tryWaitForLauncherObject(
308                     By.clazz("com.android.launcher3.widget.PendingAppWidgetHostView"), timeout);
309             return widget != null ? new Widget(mLauncher, widget) : null;
310         }
311     }
312 }