• 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_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             flingForwardUntilClearAllVisibleImpl();
161 
162             final Runnable clickClearAll = () -> mLauncher.clickLauncherObject(
163                     mLauncher.waitForObjectInContainer(verifyActiveContainer(),
164                             clearAllSelector));
165             if (mLauncher.is3PLauncher()) {
166                 mLauncher.executeAndWaitForLauncherStop(
167                         clickClearAll,
168                         "clicking 'Clear All'");
169             } else {
170                 mLauncher.runToState(
171                         clickClearAll,
172                         NORMAL_STATE_ORDINAL,
173                         "clicking 'Clear All'");
174             }
175 
176             mLauncher.waitUntilLauncherObjectGone(clearAllSelector);
177         }
178     }
179 
180     /**
181      * Scrolls until Clear-all button is visible.
182      */
flingForwardUntilClearAllVisible()183     public void flingForwardUntilClearAllVisible() {
184         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
185             flingForwardUntilClearAllVisibleImpl();
186         }
187     }
188 
flingForwardUntilClearAllVisibleImpl()189     private void flingForwardUntilClearAllVisibleImpl() {
190         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
191                 "flinging forward to clear all")) {
192             final BySelector clearAllSelector = mLauncher.getOverviewObjectSelector("clear_all");
193             for (int i = 0; i < FLINGS_FOR_DISMISS_LIMIT && !verifyActiveContainer().hasObject(
194                     clearAllSelector); ++i) {
195                 flingForwardImpl();
196             }
197         }
198     }
199 
200     /**
201      * Touch to the right of current task. This should dismiss overview and go back to Workspace.
202      */
touchOutsideFirstTask()203     public Workspace touchOutsideFirstTask() {
204         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
205              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
206                      "touching outside the focused task")) {
207 
208             if (getTaskCount() < 2) {
209                 throw new IllegalStateException(
210                         "Need to have at least 2 tasks");
211             }
212 
213             OverviewTask currentTask = flingToFirstTask();
214 
215             mLauncher.runToState(
216                     () -> mLauncher.touchOutsideContainer(currentTask.getUiObject(),
217                             /* tapRight= */ true,
218                             /* halfwayToEdge= */ false),
219                     NORMAL_STATE_ORDINAL,
220                     "touching outside of first task");
221 
222             return new Workspace(mLauncher);
223         }
224     }
225 
226     /**
227      * Touch between two tasks
228      */
touchBetweenTasks()229     public void touchBetweenTasks() {
230         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
231              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
232                      "touching outside the focused task")) {
233             if (getTaskCount() < 2) {
234                 throw new IllegalStateException(
235                         "Need to have at least 2 tasks");
236             }
237 
238             OverviewTask currentTask = flingToFirstTask();
239 
240             mLauncher.touchOutsideContainer(currentTask.getUiObject(),
241                     /* tapRight= */ false,
242                     /* halfwayToEdge= */ false);
243         }
244     }
245 
246     /**
247      * Touch either on the right or the left corner of the screen, 1 pixel from the bottom and
248      * from the sides.
249      */
touchTaskbarBottomCorner(boolean tapRight)250     public void touchTaskbarBottomCorner(boolean tapRight) {
251         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
252             Taskbar taskbar = new Taskbar(mLauncher);
253             if (mLauncher.isTransientTaskbar()) {
254                 mLauncher.runToState(
255                         () -> taskbar.touchBottomCorner(tapRight),
256                         NORMAL_STATE_ORDINAL,
257                         "touching taskbar");
258                 // Tapping outside Transient Taskbar returns to Workspace, wait for that state.
259                 new Workspace(mLauncher);
260             } else {
261                 taskbar.touchBottomCorner(tapRight);
262                 // Should stay in Overview.
263                 verifyActiveContainer();
264                 verifyActionsViewVisibility();
265             }
266         }
267     }
268 
269     /**
270      * Scrolls the current task via flinging forward until it is off screen.
271      *
272      * If only one task is present, it is only partially scrolled off screen and will still be
273      * the current task.
274      */
scrollCurrentTaskOffScreen()275     public void scrollCurrentTaskOffScreen() {
276         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
277              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
278                      "want to scroll current task off screen in overview")) {
279             verifyActiveContainer();
280 
281             OverviewTask task = getCurrentTask();
282             mLauncher.assertNotNull("current task is null", task);
283             mLauncher.scrollLeftByDistance(verifyActiveContainer(),
284                     mLauncher.getRealDisplaySize().x - task.getUiObject().getVisibleBounds().left
285                             + mLauncher.getOverviewPageSpacing());
286 
287             try (LauncherInstrumentation.Closable c2 =
288                          mLauncher.addContextLayer("scrolled task off screen")) {
289                 verifyActiveContainer();
290                 verifyActionsViewVisibility();
291 
292                 if (getTaskCount() > 1) {
293                     if (mLauncher.isTablet()) {
294                         mLauncher.assertTrue("current task is not grid height",
295                                 getCurrentTask().getVisibleHeight() == mLauncher
296                                         .getOverviewGridTaskSize().height());
297                     }
298                     mLauncher.assertTrue("Current task not scrolled off screen",
299                             !getCurrentTask().equals(task));
300                 }
301             }
302         }
303     }
304 
305     /**
306      * Gets the current task in the carousel, or fails if the carousel is empty.
307      *
308      * @return the task in the middle of the visible tasks list.
309      */
310     @NonNull
getCurrentTask()311     public OverviewTask getCurrentTask() {
312         UiObject2 currentTask = getCurrentTaskUnchecked();
313         mLauncher.assertNotNull("Unable to find a task", currentTask);
314         return new OverviewTask(mLauncher, currentTask, this);
315     }
316 
317     @Nullable
getCurrentTaskUnchecked()318     private UiObject2 getCurrentTaskUnchecked() {
319         final List<UiObject2> taskViews = getTasks();
320         if (taskViews.isEmpty()) {
321             return null;
322         }
323 
324         // The widest, and most top-right task should be the current task
325         return Collections.max(taskViews,
326                 Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
327                         .thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
328                         .thenComparing(Comparator.comparing(
329                                 (UiObject2 t) -> t.getVisibleCenter().y).reversed()));
330     }
331 
332     /**
333      * Returns an overview task that contains the specified test activity in its thumbnails.
334      *
335      * @param activityIndex index of TestActivity to match against
336      */
337     @NonNull
getTestActivityTask(int activityIndex)338     public OverviewTask getTestActivityTask(int activityIndex) {
339         return getTestActivityTask(Collections.singleton(activityIndex));
340     }
341 
342     /**
343      * Returns an overview task that contains all the specified test activities in its thumbnails.
344      *
345      * @param activityNumbers collection of indices of TestActivity to match against
346      */
347     @NonNull
getTestActivityTask(Collection<Integer> activityNumbers)348     public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) {
349         final List<UiObject2> taskViews = getTasks();
350         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
351 
352         Optional<UiObject2> task = taskViews.stream().filter(
353                 taskView -> activityNumbers.stream().allMatch(activityNumber ->
354                     // TODO(b/239452415): Use equals instead of descEndsWith
355                     taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber))
356                 )).findFirst();
357 
358         mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers
359                 + " from the task list", task.isPresent());
360 
361         return new OverviewTask(mLauncher, task.get(), this);
362     }
363 
364     /**
365      * Returns a list of all tasks fully visible in the tablet grid overview.
366      */
367     @NonNull
getCurrentTasksForTablet()368     public List<OverviewTask> getCurrentTasksForTablet() {
369         final List<UiObject2> taskViews = getTasks();
370         mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
371 
372         final int gridTaskWidth = mLauncher.getOverviewGridTaskSize().width();
373 
374         return taskViews.stream().filter(t -> t.getVisibleBounds().width() == gridTaskWidth).map(
375                 t -> new OverviewTask(mLauncher, t, this)).collect(Collectors.toList());
376     }
377 
378     @NonNull
getTasks()379     private List<UiObject2> getTasks() {
380         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
381                 "want to get overview tasks")) {
382             verifyActiveContainer();
383             return mLauncher.getDevice().findObjects(TASK_SELECTOR);
384         }
385     }
386 
getTaskCount()387     int getTaskCount() {
388         return getTasks().size();
389     }
390 
391     /**
392      * Returns whether Overview has tasks.
393      */
hasTasks()394     public boolean hasTasks() {
395         return getTasks().size() > 0;
396     }
397 
398     /**
399      * Gets Overview Actions.
400      *
401      * @return The Overview Actions
402      */
403     @NonNull
getOverviewActions()404     public OverviewActions getOverviewActions() {
405         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
406                 "want to get overview actions")) {
407             verifyActiveContainer();
408             UiObject2 overviewActions = mLauncher.waitForOverviewObject("action_buttons");
409             return new OverviewActions(overviewActions, mLauncher);
410         }
411     }
412 
413     /**
414      * Returns if clear all button is visible.
415      */
isClearAllVisible()416     public boolean isClearAllVisible() {
417         return verifyActiveContainer().hasObject(
418                 mLauncher.getOverviewObjectSelector("clear_all"));
419     }
420 
421     /**
422      * Returns the taskbar if it's a tablet, or {@code null} otherwise.
423      */
424     @Nullable
getTaskbar()425     public Taskbar getTaskbar() {
426         if (!mLauncher.isTablet()) {
427             return null;
428         }
429         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
430                 "want to get the taskbar")) {
431             mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
432 
433             return new Taskbar(mLauncher);
434         }
435     }
436 
isActionsViewVisible()437     protected boolean isActionsViewVisible() {
438         if (!hasTasks() || isClearAllVisible()) {
439             testLogD(TAG, "Not expecting an actions bar: no tasks/'Clear all' is visible");
440             return false;
441         }
442         boolean isTablet = mLauncher.isTablet();
443         if (isTablet && mLauncher.isGridOnlyOverviewEnabled()) {
444             testLogD(TAG, "Not expecting an actions bar: device is tablet with grid-only Overview");
445             return false;
446         }
447         OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
448         if (task == null) {
449             testLogD(TAG, "Not expecting an actions bar: no current task");
450             return false;
451         }
452         // In tablets, if focused task is not in center, overview actions aren't visible.
453         if (isTablet && Math.abs(task.getExactCenterX() - mLauncher.getExactScreenCenterX()) >= 1) {
454             testLogD(TAG,
455                     "Not expecting an actions bar: device is tablet and task is not centered");
456             return false;
457         }
458         if (task.isGrouped() && !isTablet) {
459             testLogD(TAG, "Not expecting an actions bar: device is phone and task is split");
460             // Overview actions aren't visible for split screen tasks, except for save app pair
461             // button on tablets.
462             return false;
463         }
464         testLogD(TAG, "Expecting an actions bar");
465         return true;
466     }
467 
468     /**
469      * Presses the esc key to dismiss Overview.
470      */
dismissByEscKey()471     public Workspace dismissByEscKey() {
472         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
473             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ALT_ESC_UP);
474             mLauncher.runToState(
475                     () -> mLauncher.getDevice().pressKeyCode(KEYCODE_ESCAPE),
476                     NORMAL_STATE_ORDINAL, "pressing esc key");
477             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
478                     "pressed esc key")) {
479                 return mLauncher.getWorkspace();
480             }
481         }
482     }
483 
484     /**
485      * Presses the enter key to launch the focused task
486      * <p>
487      * If no task is focused, this will fail.
488      */
launchFocusedTaskByEnterKey(@onNull String expectedPackageName)489     public LaunchedAppState launchFocusedTaskByEnterKey(@NonNull String expectedPackageName) {
490         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
491             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ENTER_UP);
492             mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
493 
494             mLauncher.executeAndWaitForLauncherStop(
495                     () -> mLauncher.assertTrue(
496                             "Failed to press enter",
497                             mLauncher.getDevice().pressKeyCode(KeyEvent.KEYCODE_ENTER)),
498                     "pressing enter");
499             mLauncher.assertAppLaunched(expectedPackageName);
500 
501             try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
502                     "pressed enter")) {
503                 return new LaunchedAppState(mLauncher);
504             }
505         }
506     }
507 
verifyActionsViewVisibility()508     private void verifyActionsViewVisibility() {
509         // If no running tasks, no need to verify actions view visibility.
510         if (getTasks().isEmpty()) {
511             return;
512         }
513 
514         boolean isTablet = mLauncher.isTablet();
515         OverviewTask task = isTablet ? getFocusedTaskForTablet() : getCurrentTask();
516 
517         try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
518                 "want to assert overview actions view visibility="
519                         + isActionsViewVisible()
520                         + ", focused task is "
521                         + (task == null ? "null" : (task.isGrouped() ? "split" : "not split"))
522                 )) {
523 
524             if (isActionsViewVisible()) {
525                 if (task.isGrouped()) {
526                     mLauncher.waitForOverviewObject("action_save_app_pair");
527                 } else {
528                     mLauncher.waitForOverviewObject("action_buttons");
529                 }
530             } else {
531                 mLauncher.waitUntilOverviewObjectGone("action_buttons");
532                 mLauncher.waitUntilOverviewObjectGone("action_save_app_pair");
533             }
534         }
535     }
536 
537     /**
538      * Returns Overview focused task if it exists.
539      *
540      * @throws IllegalStateException if not run on a tablet device.
541      */
getFocusedTaskForTablet()542     OverviewTask getFocusedTaskForTablet() {
543         if (!mLauncher.isTablet()) {
544             throw new IllegalStateException("Must be run on tablet device.");
545         }
546         final List<UiObject2> taskViews = getTasks();
547         if (taskViews.isEmpty()) {
548             return null;
549         }
550         Rect focusTaskSize = mLauncher.getOverviewTaskSize();
551         int focusedTaskHeight = focusTaskSize.height();
552         for (UiObject2 task : taskViews) {
553             OverviewTask overviewTask = new OverviewTask(mLauncher, task, this);
554             // Desktop tasks can't be focused tasks, but are the same size.
555             if (overviewTask.isDesktop()) {
556                 continue;
557             }
558             if (overviewTask.getVisibleHeight() == focusedTaskHeight) {
559                 return overviewTask;
560             }
561         }
562         return null;
563     }
564 
isLiveTile(UiObject2 task)565     protected boolean isLiveTile(UiObject2 task) {
566         // UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence
567         // compare only hashCode as a workaround.
568         return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode();
569     }
570 }
571