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_ESCAPE;
20 
21 import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
22 import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
23 import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
24 
25 import android.graphics.Rect;
26 import android.util.Log;
27 import android.view.KeyEvent;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.test.uiautomator.By;
32 import androidx.test.uiautomator.BySelector;
33 import androidx.test.uiautomator.Direction;
34 import androidx.test.uiautomator.UiObject2;
35 
36 import com.android.launcher3.testing.shared.TestProtocol;
37 
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.List;
41 import java.util.regex.Pattern;
42 import java.util.stream.Collectors;
43 
44 /**
45  * Common overview panel for both Launcher and fallback recents
46  */
47 public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
48     protected static final String TASK_RES_ID = "task";
49     private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
50             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
51     private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
52             "Key event: KeyEvent.*?action=ACTION_DOWN.*?keyCode=KEYCODE_ENTER");
53     private static final Pattern EVENT_ENTER_UP = Pattern.compile(
54             "Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ENTER");
55 
56     private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
57 
BaseOverview(LauncherInstrumentation launcher)58     BaseOverview(LauncherInstrumentation launcher) {
59         super(launcher);
60         verifyActiveContainer();
61         verifyActionsViewVisibility();
62     }
63 
64     @Override
getContainerType()65     protected LauncherInstrumentation.ContainerType getContainerType() {
66         return LauncherInstrumentation.ContainerType.FALLBACK_OVERVIEW;
67     }
68 
69     /**
70      * Flings forward (left) and waits the fling's end.
71      */
flingForward()72     public void flingForward() {
73         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
74             flingForwardImpl();
75         }
76     }
77 
flingForwardImpl()78     private void flingForwardImpl() {
79         try (LauncherInstrumentation.Closable c =
80                      mLauncher.addContextLayer("want to fling forward in overview")) {
81             LauncherInstrumentation.log("Overview.flingForward before fling");
82             final UiObject2 overview = verifyActiveContainer();
83             final int leftMargin =
84                     mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth();
85             mLauncher.scroll(overview, Direction.LEFT, new Rect(leftMargin + 1, 0, 0, 0), 20,
86                     false);
87             try (LauncherInstrumentation.Closable c2 =
88                          mLauncher.addContextLayer("flung forwards")) {
89                 verifyActiveContainer();
90                 verifyActionsViewVisibility();
91             }
92         }
93     }
94 
95     /**
96      * Flings backward (right) and waits the fling's end.
97      */
flingBackward()98     public void flingBackward() {
99         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
100             flingBackwardImpl();
101         }
102     }
103 
flingBackwardImpl()104     private void flingBackwardImpl() {
105         try (LauncherInstrumentation.Closable c =
106                      mLauncher.addContextLayer("want to fling backward in overview")) {
107             LauncherInstrumentation.log("Overview.flingBackward before fling");
108             final UiObject2 overview = verifyActiveContainer();
109             final int rightMargin =
110                     mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth();
111             mLauncher.scroll(
112                     overview, Direction.RIGHT, new Rect(0, 0, rightMargin + 1, 0), 20, false);
113             try (LauncherInstrumentation.Closable c2 =
114                          mLauncher.addContextLayer("flung backwards")) {
115                 verifyActiveContainer();
116                 verifyActionsViewVisibility();
117             }
118         }
119     }
120 
flingToFirstTask()121     private OverviewTask flingToFirstTask() {
122         OverviewTask currentTask = getCurrentTask();
123 
124         while (mLauncher.getRealDisplaySize().x - currentTask.getUiObject().getVisibleBounds().right
125                 <= mLauncher.getOverviewPageSpacing()) {
126             flingBackwardImpl();
127             currentTask = getCurrentTask();
128         }
129 
130         return currentTask;
131     }
132 
133     /**
134      * Dismissed all tasks by scrolling to Clear-all button and pressing it.
135      */
dismissAllTasks()136     public void dismissAllTasks() {
137         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
138              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
139                      "dismissing all tasks")) {
140             final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
141             for (int i = 0;
142                     i < FLINGS_FOR_DISMISS_LIMIT
143                             && !verifyActiveContainer().hasObject(clearAllSelector);
144                     ++i) {
145                 flingForwardImpl();
146             }
147 
148             final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
149                     mLauncher.waitForObjectInContainer(verifyActiveContainer(),
150                             clearAllSelector));
151             if (mLauncher.is3PLauncher()) {
152                 mLauncher.executeAndWaitForLauncherStop(
153                         clickClearAll,
154                         "clicking 'Clear All'");
155             } else {
156                 mLauncher.runToState(
157                         clickClearAll,
158                         NORMAL_STATE_ORDINAL,
159                         "clicking 'Clear All'");
160             }
161 
162             mLauncher.waitUntilLauncherObjectGone(clearAllSelector);
163         }
164     }
165 
166     /**
167      * Touch to the right of current task. This should dismiss overview and go back to Workspace.
168      */
touchOutsideFirstTask()169     public Workspace touchOutsideFirstTask() {
170         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
171              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
172                      "touching outside the focused task")) {
173 
174             if (getTaskCount() < 2) {
175                 throw new IllegalStateException(
176                         "Need to have at least 2 tasks");
177             }
178 
179             OverviewTask currentTask = flingToFirstTask();
180 
181             mLauncher.runToState(
182                     () -> mLauncher.touchOutsideContainer(currentTask.getUiObject(),
183                             /* tapRight= */ true,
184                             /* halfwayToEdge= */ false),
185                     NORMAL_STATE_ORDINAL,
186                     "touching outside of first task");
187 
188             return new Workspace(mLauncher);
189         }
190     }
191 
192     /**
193      * Touch between two tasks
194      */
touchBetweenTasks()195     public void touchBetweenTasks() {
196         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
197              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
198                      "touching outside the focused task")) {
199             if (getTaskCount() < 2) {
200                 throw new IllegalStateException(
201                         "Need to have at least 2 tasks");
202             }
203 
204             OverviewTask currentTask = flingToFirstTask();
205 
206             mLauncher.touchOutsideContainer(currentTask.getUiObject(),
207                     /* tapRight= */ false,
208                     /* halfwayToEdge= */ false);
209         }
210     }
211 
212     /**
213      * Touch either on the right or the left corner of the screen, 1 pixel from the bottom and
214      * from the sides.
215      */
touchTaskbarBottomCorner(boolean tapRight)216     public void touchTaskbarBottomCorner(boolean tapRight) {
217         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
218             Taskbar taskbar = new Taskbar(mLauncher);
219             if (mLauncher.isTransientTaskbar()) {
220                 mLauncher.runToState(
221                         () -> taskbar.touchBottomCorner(tapRight),
222                         NORMAL_STATE_ORDINAL,
223                         "touching taskbar");
224                 // Tapping outside Transient Taskbar returns to Workspace, wait for that state.
225                 new Workspace(mLauncher);
226             } else {
227                 taskbar.touchBottomCorner(tapRight);
228                 // Should stay in Overview.
229                 verifyActiveContainer();
230                 verifyActionsViewVisibility();
231             }
232         }
233     }
234 
235     /**
236      * Scrolls the current task via flinging forward until it is off screen.
237      *
238      * If only one task is present, it is only partially scrolled off screen and will still be
239      * the current task.
240      */
scrollCurrentTaskOffScreen()241     public void scrollCurrentTaskOffScreen() {
242         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
243              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
244                      "want to scroll current task off screen in overview")) {
245             verifyActiveContainer();
246 
247             OverviewTask task = getCurrentTask();
248             mLauncher.assertNotNull("current task is null", task);
249             mLauncher.scrollLeftByDistance(verifyActiveContainer(),
250                     mLauncher.getRealDisplaySize().x - task.getUiObject().getVisibleBounds().left
251                             + mLauncher.getOverviewPageSpacing());
252 
253             try (LauncherInstrumentation.Closable c2 =
254                          mLauncher.addContextLayer("scrolled task off screen")) {
255                 verifyActiveContainer();
256                 verifyActionsViewVisibility();
257 
258                 if (getTaskCount() > 1) {
259                     if (mLauncher.isTablet()) {
260                         mLauncher.assertTrue("current task is not grid height",
261                                 getCurrentTask().getVisibleHeight() == mLauncher
262                                         .getGridTaskRectForTablet().height());
263                     }
264                     mLauncher.assertTrue("Current task not scrolled off screen",
265                             !getCurrentTask().equals(task));
266                 }
267             }
268         }
269     }
270 
271     /**
272      * Gets the current task in the carousel, or fails if the carousel is empty.
273      *
274      * @return the task in the middle of the visible tasks list.
275      */
276     @NonNull
getCurrentTask()277     public OverviewTask getCurrentTask() {
278         final List<UiObject2> taskViews = getTasks();
279         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
280 
281         // The widest, and most top-right task should be the current task
282         UiObject2 currentTask = Collections.max(taskViews,
283                 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
284                         .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
285                         .thenComparing(Comparator.comparing(
286                                 (UiObject2 t) -> t.getVisibleCenter().y).reversed()));
287         return new OverviewTask(mLauncher, currentTask, this);
288     }
289 
290     /** Returns an overview task matching TestActivity {@param activityNumber}. */
291     @NonNull
getTestActivityTask(int activityNumber)292     public OverviewTask getTestActivityTask(int activityNumber) {
293         final List<UiObject2> taskViews = getTasks();
294         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
295 
296         final String activityName = "TestActivity" + activityNumber;
297         UiObject2 task = null;
298         for (UiObject2 taskView : taskViews) {
299             // TODO(b/239452415): Use equals instead of descEndsWith
300             if (taskView.getParent().hasObject(By.descEndsWith(activityName))) {
301                 task = taskView;
302                 break;
303             }
304         }
305         mLauncher.assertNotNull(
306                 "Unable to find a task with " + activityName + " from the task list", task);
307 
308         return new OverviewTask(mLauncher, task, this);
309     }
310 
311     /**
312      * Returns a list of all tasks fully visible in the tablet grid overview.
313      */
314     @NonNull
getCurrentTasksForTablet()315     public List<OverviewTask> getCurrentTasksForTablet() {
316         final List<UiObject2> taskViews = getTasks();
317         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
318 
319         final int gridTaskWidth = mLauncher.getGridTaskRectForTablet().width();
320 
321         return taskViews.stream().filter(t -> t.getVisibleBounds().width() == gridTaskWidth).map(
322                 t -> new OverviewTask(mLauncher, t, this)).collect(Collectors.toList());
323     }
324 
325     @NonNull
getTasks()326     private List<UiObject2> getTasks() {
327         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
328                 "want to get overview tasks")) {
329             verifyActiveContainer();
330             return mLauncher.getDevice().findObjects(
331                     mLauncher.getOverviewObjectSelector(TASK_RES_ID));
332         }
333     }
334 
335 
getTaskCount()336     int getTaskCount() {
337         return getTasks().size();
338     }
339 
340     /**
341      * Returns whether Overview has tasks.
342      */
hasTasks()343     public boolean hasTasks() {
344         return getTasks().size() > 0;
345     }
346 
347     /**
348      * Gets Overview Actions.
349      *
350      * @return The Overview Actions
351      */
352     @NonNull
getOverviewActions()353     public OverviewActions getOverviewActions() {
354         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
355                 "want to get overview actions")) {
356             verifyActiveContainer();
357             UiObject2 overviewActions = mLauncher.waitForOverviewObject("action_buttons");
358             return new OverviewActions(overviewActions, mLauncher);
359         }
360     }
361 
362     /**
363      * Returns if clear all button is visible.
364      */
isClearAllVisible()365     public boolean isClearAllVisible() {
366         return verifyActiveContainer().hasObject(
367                 mLauncher.getOverviewObjectSelector("clear_all"));
368     }
369 
370     /**
371      * Returns the taskbar if it's a tablet, or {@code null} otherwise.
372      */
373     @Nullable
getTaskbar()374     public Taskbar getTaskbar() {
375         if (!mLauncher.isTablet()) {
376             return null;
377         }
378         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
379                 "want to get the taskbar")) {
380             mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
381 
382             return new Taskbar(mLauncher);
383         }
384     }
385 
isActionsViewVisible()386     protected boolean isActionsViewVisible() {
387         boolean hasTasks = hasTasks();
388         if (!hasTasks || isClearAllVisible()) {
389             LauncherInstrumentation.log("Not expecting an actions bar:"
390                     + (!hasTasks ? "no recent tasks" : "clear all button is visible"));
391             return false;
392         }
393         boolean isTablet = mLauncher.isTablet();
394         if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) {
395             LauncherInstrumentation.log("Not expecting an actions bar: "
396                     + "device is tablet with grid-only Overview");
397             return false;
398         }
399         OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
400         if (task == null) {
401             LauncherInstrumentation.log("Not expecting an actions bar: no focused task");
402             return false;
403         }
404         float centerOffset = Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX());
405         // In tablets, if focused task is not in center, overview actions aren't visible.
406         if (isTablet && centerOffset >= 1) {
407             LauncherInstrumentation.log("Not expecting an actions bar: "
408                     + "device is tablet and task is not centered; center offset by "
409                     + centerOffset + "px");
410             return false;
411         }
412         if (task.isTaskSplit() && (!mLauncher.isAppPairsEnabled() || !isTablet)) {
413             LauncherInstrumentation.log("Not expecting an actions bar: "
414                     + "device is phone and task is split");
415             // Overview actions aren't visible for split screen tasks, except for save app pair
416             // button on tablets.
417             return false;
418         }
419         LauncherInstrumentation.log("Expecting an actions bar");
420         return true;
421     }
422 
423     /**
424      * Presses the esc key to dismiss Overview.
425      */
dismissByEscKey()426     public Workspace dismissByEscKey() {
427         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
428             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
429             mLauncher.runToState(
430                     () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE),
431                     NORMAL_STATE_ORDINAL, "pressing esc key");
432             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
433                     "pressed esc key")) {
434                 return mLauncher.getWorkspace();
435             }
436         }
437     }
438 
439     /**
440      * Presses the enter key to launch the focused task
441      * <p>
442      * If no task is focused, this will fail.
443      */
launchFocusedTaskByEnterKey(@onNull String expectedPackageName)444     public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) {
445         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
446             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP);
447             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
448 
449             mLauncher.executeAndWaitForLauncherStop(
450                     () -> mLauncher.assertTrue(
451                             "Failed to press enter",
452                             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)),
453                     "pressing enter");
454             mLauncher.assertAppLaunched(expectedPackageName);
455 
456             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
457                     "pressed enter")) {
458                 return new LaunchedAppState(mLauncher);
459             }
460         }
461     }
462 
verifyActionsViewVisibility()463     private void verifyActionsViewVisibility() {
464         // If no running tasks, no need to verify actions view visibility.
465         if (getTasks().isEmpty()) {
466             return;
467         }
468 
469         boolean isTablet = mLauncher.isTablet();
470         OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
471 
472         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
473                 "want to assert overview actions view visibility="
474                         + isActionsViewVisible()
475                         + ", focused task is "
476                         + (task == null ? "null" : (task.isTaskSplit() ? "split" : "not split"))
477                 )) {
478 
479             if (isActionsViewVisible()) {
480                 if (task.isTaskSplit()) {
481                     mLauncher.waitForOverviewObject("action_save_app_pair");
482                 } else {
483                     mLauncher.waitForOverviewObject("action_buttons");
484                 }
485             } else {
486                 mLauncher.waitUntilOverviewObjectGone("action_buttons");
487                 mLauncher.waitUntilOverviewObjectGone("action_save_app_pair");
488             }
489         }
490     }
491 
492     /**
493      * Returns Overview focused task if it exists.
494      *
495      * @throws IllegalStateException if not run on a tablet device.
496      */
getFocusedTaskForTablet()497     OverviewTask getFocusedTaskForTablet() {
498         if (!mLauncher.isTablet()) {
499             throw new IllegalStateException("Must be run on tablet device.");
500         }
501         final List<UiObject2> taskViews = getTasks();
502         if (!hasTasks()) {
503             LauncherInstrumentation.log("no recent tasks");
504             return null;
505         }
506         int focusedTaskHeight = mLauncher.getFocusedTaskHeightForTablet();
507         for (UiObject2 task : taskViews) {
508             OverviewTask overviewTask = new OverviewTask(mLauncher, task, this);
509 
510             LauncherInstrumentation.log("checking task height ("
511                     + overviewTask.getVisibleHeight()
512                     + ") against defined focused task height ("
513                     + focusedTaskHeight + ")");
514             if (overviewTask.getVisibleHeight() == focusedTaskHeight) {
515                 return overviewTask;
516             }
517         }
518         return null;
519     }
520 }
521