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