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.tapl.OverviewTask.OverviewSplitTask.DEFAULT;
20 import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT;
21 import static com.android.launcher3.tapl.OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT;
22 
23 import android.graphics.Rect;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.test.uiautomator.By;
28 import androidx.test.uiautomator.BySelector;
29 import androidx.test.uiautomator.UiObject2;
30 
31 import com.android.launcher3.testing.shared.TestProtocol;
32 
33 import java.util.List;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36 
37 /**
38  * A recent task in the overview panel carousel.
39  */
40 public final class OverviewTask {
41     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
42     static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
43     static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
44     static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
45     private final LauncherInstrumentation mLauncher;
46     private final UiObject2 mTask;
47     private final BaseOverview mOverview;
48 
OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview)49     OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) {
50         mLauncher = launcher;
51         mTask = task;
52         mOverview = overview;
53         verifyActiveContainer();
54     }
55 
verifyActiveContainer()56     private void verifyActiveContainer() {
57         mOverview.verifyActiveContainer();
58     }
59 
60     /**
61      * Returns the height of the visible task, or the combined height of two tasks in split with a
62      * divider between.
63      */
getVisibleHeight()64     int getVisibleHeight() {
65         if (isTaskSplit()) {
66             return getCombinedSplitTaskHeight();
67         }
68 
69         UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
70         return taskSnapshot1.getVisibleBounds().height();
71     }
72 
73     /**
74      * Calculates the visible height for split tasks, containing 2 snapshot tiles and a divider.
75      */
getCombinedSplitTaskHeight()76     private int getCombinedSplitTaskHeight() {
77         UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
78         UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
79 
80         // If the split task is partly off screen, taskSnapshot1 can be invisible.
81         if (taskSnapshot1 == null) {
82             return taskSnapshot2.getVisibleBounds().height();
83         }
84 
85         int top = Math.min(
86                 taskSnapshot1.getVisibleBounds().top, taskSnapshot2.getVisibleBounds().top);
87         int bottom = Math.max(
88                 taskSnapshot1.getVisibleBounds().bottom, taskSnapshot2.getVisibleBounds().bottom);
89 
90         return bottom - top;
91     }
92 
93     /**
94      * Returns the width of the visible task, or the combined width of two tasks in split with a
95      * divider between.
96      */
getVisibleWidth()97     int getVisibleWidth() {
98         if (isTaskSplit()) {
99             return getCombinedSplitTaskWidth();
100         }
101 
102         UiObject2 taskSnapshot1 = findObjectInTask(DEFAULT.snapshotRes);
103         return taskSnapshot1.getVisibleBounds().width();
104     }
105 
106     /**
107      * Calculates the visible width for split tasks, containing 2 snapshot tiles and a divider.
108      */
getCombinedSplitTaskWidth()109     private int getCombinedSplitTaskWidth() {
110         UiObject2 taskSnapshot1 = findObjectInTask(SPLIT_TOP_OR_LEFT.snapshotRes);
111         UiObject2 taskSnapshot2 = findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes);
112 
113         int left = Math.min(
114                 taskSnapshot1.getVisibleBounds().left, taskSnapshot2.getVisibleBounds().left);
115         int right = Math.max(
116                 taskSnapshot1.getVisibleBounds().right, taskSnapshot2.getVisibleBounds().right);
117 
118         return right - left;
119     }
120 
getTaskCenterX()121     int getTaskCenterX() {
122         return mTask.getVisibleCenter().x;
123     }
124 
getTaskCenterY()125     int getTaskCenterY() {
126         return mTask.getVisibleCenter().y;
127     }
128 
getExactCenterX()129     float getExactCenterX() {
130         return mTask.getVisibleBounds().exactCenterX();
131     }
132 
getUiObject()133     UiObject2 getUiObject() {
134         return mTask;
135     }
136 
137     /**
138      * Dismisses the task by swiping up.
139      */
dismiss()140     public void dismiss() {
141         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
142              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
143                      "want to dismiss an overview task")) {
144             verifyActiveContainer();
145             int taskCountBeforeDismiss = mOverview.getTaskCount();
146             mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss);
147             if (taskCountBeforeDismiss == 1) {
148                 dismissBySwipingUp();
149                 return;
150             }
151 
152             boolean taskWasFocused = mLauncher.isTablet() && getVisibleHeight() == mLauncher
153                     .getFocusedTaskHeightForTablet();
154             List<Integer> originalTasksCenterX =
155                     getCurrentTasksCenterXList().stream().sorted().toList();
156             boolean isClearAllVisibleBeforeDismiss = mOverview.isClearAllVisible();
157 
158             dismissBySwipingUp();
159 
160             try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer("dismissed")) {
161                 if (taskWasFocused) {
162                     mLauncher.assertNotNull("No task became focused",
163                             mOverview.getFocusedTaskForTablet());
164                 }
165                 if (!isClearAllVisibleBeforeDismiss) {
166                     List<Integer> currentTasksCenterX =
167                             getCurrentTasksCenterXList().stream().sorted().toList();
168                     if (originalTasksCenterX.size() == currentTasksCenterX.size()) {
169                         // Check for the same number of visible tasks before and after to
170                         // avoid asserting on cases of shifting all tasks to close the distance
171                         // between clear all and tasks at the end of the grid.
172                         mLauncher.assertTrue("Task centers not aligned",
173                                 originalTasksCenterX.equals(currentTasksCenterX));
174                     }
175                 }
176             }
177         }
178     }
179 
dismissBySwipingUp()180     private void dismissBySwipingUp() {
181         verifyActiveContainer();
182         // Dismiss the task via flinging it up.
183         final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
184         final int centerX = taskBounds.centerX();
185         final int centerY = taskBounds.bottom - 1;
186         mLauncher.executeAndWaitForLauncherEvent(
187                 () -> mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
188                         LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
189                 event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(event.getClassName()),
190                 () -> "Didn't receive a dismiss animation ends message: " + centerX + ", "
191                         + centerY, "swiping to dismiss");
192     }
193 
getCurrentTasksCenterXList()194     private List<Integer> getCurrentTasksCenterXList() {
195         return mLauncher.isTablet()
196                 ? mOverview.getCurrentTasksForTablet().stream()
197                 .map(OverviewTask::getTaskCenterX)
198                 .collect(Collectors.toList())
199                 : List.of(mOverview.getCurrentTask().getTaskCenterX());
200     }
201 
202     /**
203      * Clicks the task.
204      */
open()205     public LaunchedAppState open() {
206         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
207             verifyActiveContainer();
208             mLauncher.executeAndWaitForLauncherStop(
209                     () -> mLauncher.clickLauncherObject(mTask),
210                     "clicking an overview task");
211             if (mOverview.getContainerType()
212                     == LauncherInstrumentation.ContainerType.SPLIT_SCREEN_SELECT) {
213                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SPLIT_START_EVENT);
214 
215                 try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
216                         "launched splitscreen")) {
217 
218                     BySelector divider = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle");
219                     mLauncher.waitForSystemUiObject(divider);
220                     return new LaunchedAppState(mLauncher);
221                 }
222             } else {
223                 mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
224                 return new LaunchedAppState(mLauncher);
225             }
226         }
227     }
228 
229     /** Taps the task menu. Returns the task menu object. */
230     @NonNull
tapMenu()231     public OverviewTaskMenu tapMenu() {
232         return tapMenu(DEFAULT);
233     }
234 
235     /** Taps the task menu of the split task. Returns the split task's menu object. */
236     @NonNull
tapMenu(OverviewSplitTask task)237     public OverviewTaskMenu tapMenu(OverviewSplitTask task) {
238         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
239              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
240                      "want to tap the task menu")) {
241             mLauncher.clickLauncherObject(
242                     mLauncher.waitForObjectInContainer(mTask, task.iconAppRes));
243 
244             try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
245                     "tapped the task menu")) {
246                 return new OverviewTaskMenu(mLauncher);
247             }
248         }
249     }
250 
isTaskSplit()251     boolean isTaskSplit() {
252         return findObjectInTask(SPLIT_BOTTOM_OR_RIGHT.snapshotRes) != null;
253     }
254 
findObjectInTask(String resName)255     private UiObject2 findObjectInTask(String resName) {
256         return mTask.findObject(mLauncher.getOverviewObjectSelector(resName));
257     }
258 
259     /**
260      * Returns whether the given String is contained in this Task's contentDescription. Also returns
261      * true if both Strings are null.
262      *
263      * TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
264      */
containsContentDescription(@ullable String expected)265     public boolean containsContentDescription(@Nullable String expected) {
266         String actual = mTask.getContentDescription();
267         if (actual == null && expected == null) {
268             return true;
269         }
270         if (actual == null || expected == null) {
271             return false;
272         }
273         return actual.contains(expected);
274     }
275 
276     /**
277      * Enum used to specify  which task is retrieved when it is a split task.
278      */
279     public enum OverviewSplitTask {
280         // The main task when the task is not split.
281         DEFAULT("snapshot", "icon"),
282         // The first task in split task.
283         SPLIT_TOP_OR_LEFT("snapshot", "icon"),
284         // The second task in split task.
285         SPLIT_BOTTOM_OR_RIGHT("bottomright_snapshot", "bottomRight_icon");
286 
287         public final String snapshotRes;
288         public final String iconAppRes;
289 
OverviewSplitTask(String snapshotRes, String iconAppRes)290         OverviewSplitTask(String snapshotRes, String iconAppRes) {
291             this.snapshotRes = snapshotRes;
292             this.iconAppRes = iconAppRes;
293         }
294     }
295 }
296