1 /*
2  * Copyright (C) 2022 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 android.platform.spectatio.configs;
18 
19 import android.platform.spectatio.constants.JsonConfigConstants.FindType;
20 import android.platform.spectatio.constants.JsonConfigConstants.ScrollActions;
21 import android.platform.spectatio.constants.JsonConfigConstants.ScrollDirection;
22 import android.platform.spectatio.constants.JsonConfigConstants.SupportedWorkFlowTasks;
23 import android.platform.spectatio.exceptions.MissingUiElementException;
24 import android.platform.spectatio.utils.SpectatioUiUtil;
25 import android.platform.spectatio.utils.SpectatioUiUtil.SwipeDirection;
26 import android.platform.spectatio.utils.SpectatioUiUtil.SwipeFraction;
27 import android.util.Log;
28 
29 import androidx.test.uiautomator.BySelector;
30 import androidx.test.uiautomator.UiObject2;
31 
32 import com.google.common.base.Strings;
33 import com.google.gson.annotations.SerializedName;
34 
35 /** Workflow Task For Workflows in Spectatio Config JSON Config */
36 public class WorkflowTask {
37     private static final String LOG_TAG = WorkflowTask.class.getSimpleName();
38 
39     @SerializedName("NAME")
40     private String mName;
41 
42     @SerializedName("TYPE")
43     private String mType;
44 
45     // TEXT or UI_ELEMENT based on the Type of Workflow Task
46     @SerializedName("CONFIG")
47     private WorkflowTaskConfig mTaskConfig;
48 
49     // Number of times to repeat given task, default is 0
50     // By default, task will be execute once and won't be repeated as repeat count is 0
51     @SerializedName("REPEAT_COUNT")
52     private int mRepeatCount;
53 
54     // If task needs scrolling, provide Scroll Config
55     @SerializedName("SCROLL_CONFIG")
56     private ScrollConfig mScrollConfig;
57 
58     // If task needs swiping, provide Swipe Config
59     @SerializedName("SWIPE_CONFIG")
60     private SwipeConfig mSwipeConfig;
61 
WorkflowTask( String name, String type, WorkflowTaskConfig taskConfig, int repeatCount, ScrollConfig scrollConfig, SwipeConfig swipeConfig)62     public WorkflowTask(
63             String name,
64             String type,
65             WorkflowTaskConfig taskConfig,
66             int repeatCount,
67             ScrollConfig scrollConfig,
68             SwipeConfig swipeConfig) {
69         mName = name;
70         mType = type;
71         mTaskConfig = taskConfig;
72         mRepeatCount = repeatCount;
73         mScrollConfig = scrollConfig;
74         mSwipeConfig = swipeConfig;
75     }
76 
getTaskName()77     public String getTaskName() {
78         return mName;
79     }
80 
getTaskType()81     public String getTaskType() {
82         return mType;
83     }
84 
getTaskConfig()85     public WorkflowTaskConfig getTaskConfig() {
86         return mTaskConfig;
87     }
88 
getRepeatCount()89     public int getRepeatCount() {
90         return mRepeatCount;
91     }
92 
getScrollConfig()93     public ScrollConfig getScrollConfig() {
94         return mScrollConfig;
95     }
96 
getSwipeConfig()97     public SwipeConfig getSwipeConfig() {
98         return mSwipeConfig;
99     }
100 
executeTask(String workflowName, SpectatioUiUtil spectatioUiUtil)101     public void executeTask(String workflowName, SpectatioUiUtil spectatioUiUtil) {
102         Log.i(
103                 LOG_TAG,
104                 String.format(
105                         "Executing Task %s with Type %s for Workflow %s",
106                         mName, mType, workflowName));
107 
108         SupportedWorkFlowTasks taskType =
109                 validateAndGetEnumValue(
110                         workflowName, "Workflow Task Type", SupportedWorkFlowTasks.class, mType);
111 
112         int executionCount = 0;
113         // Execute Task once by default. Repeat it again based on repeat count i.e. mRepeatCount > 0
114         do {
115             executeTask(taskType, workflowName, spectatioUiUtil);
116             executionCount++;
117             Log.i(
118                     LOG_TAG,
119                     String.format(
120                             "Completed executing Task %s, %d time(s).", mName, executionCount));
121 
122             // Wait for 1 Second before executing another task
123             spectatioUiUtil.wait1Second();
124         } while (executionCount < (1 + mRepeatCount));
125 
126         Log.i(
127                 LOG_TAG,
128                 String.format(
129                         "Done Executing Task %s with Type %s for Workflow %s",
130                         mName, mType, workflowName));
131     }
132 
executeTask( SupportedWorkFlowTasks taskType, String workflowName, SpectatioUiUtil spectatioUiUtil)133     private void executeTask(
134             SupportedWorkFlowTasks taskType, String workflowName, SpectatioUiUtil spectatioUiUtil) {
135         switch (taskType) {
136             case COMMAND:
137                 validateAndExecuteCommand(workflowName, spectatioUiUtil);
138                 break;
139             case HAS_PACKAGE_IN_FOREGROUND:
140                 validateAndVerifyPackage(workflowName, spectatioUiUtil);
141                 break;
142             case HAS_UI_ELEMENT_IN_FOREGROUND:
143                 validateAndVerifyUiElement(workflowName, spectatioUiUtil);
144                 break;
145             case CLICK:
146                 validateAndClickUiElement(
147                         workflowName,
148                         spectatioUiUtil,
149                         FindType.NONE,
150                         /* isLongClick= */ false,
151                         /* isOptional= */ false);
152                 break;
153             case CLICK_IF_EXIST:
154                 validateAndClickUiElement(
155                         workflowName,
156                         spectatioUiUtil,
157                         FindType.NONE,
158                         /* isLongClick= */ false,
159                         /* isOptional= */ true);
160                 break;
161             case LONG_CLICK:
162                 validateAndClickUiElement(
163                         workflowName,
164                         spectatioUiUtil,
165                         FindType.NONE,
166                         /* isLongClick= */ true,
167                         /* isOptional= */ false);
168                 break;
169             case PRESS:
170                 validateAndPressKey(workflowName, spectatioUiUtil, /* isLongPress= */ false);
171                 break;
172             case LONG_PRESS:
173                 validateAndPressKey(workflowName, spectatioUiUtil, /* isLongPress= */ true);
174                 break;
175             case SCROLL_TO_FIND_AND_CLICK:
176                 validateAndClickUiElement(
177                         workflowName,
178                         spectatioUiUtil,
179                         FindType.SCROLL,
180                         /* isLongClick= */ false,
181                         /* isOptional= */ false);
182                 break;
183             case SCROLL_TO_FIND_AND_CLICK_IF_EXIST:
184                 validateAndClickUiElement(
185                         workflowName,
186                         spectatioUiUtil,
187                         FindType.SCROLL,
188                         /* isLongClick= */ false,
189                         /* isOptional= */ true);
190                 break;
191             case SWIPE:
192                 validateAndSwipe(workflowName, spectatioUiUtil);
193                 break;
194             case SWIPE_TO_FIND_AND_CLICK:
195                 validateAndClickUiElement(
196                         workflowName,
197                         spectatioUiUtil,
198                         FindType.SWIPE,
199                         /* isLongClick= */ false,
200                         /* isOptional= */ false);
201                 break;
202             case SWIPE_TO_FIND_AND_CLICK_IF_EXIST:
203                 validateAndClickUiElement(
204                         workflowName,
205                         spectatioUiUtil,
206                         FindType.SWIPE,
207                         /* isLongClick= */ false,
208                         /* isOptional= */ true);
209                 break;
210             case WAIT_MS:
211                 validateAndWait(workflowName, spectatioUiUtil);
212                 break;
213             default:
214                 throwRuntimeException("Workflow Task Type", mType, workflowName, "Not Supported");
215         }
216     }
217 
validateAndWait(String workflowName, SpectatioUiUtil spectatioUiUtil)218     private void validateAndWait(String workflowName, SpectatioUiUtil spectatioUiUtil) {
219         String waitTime = validateAndGetTaskConfigText(workflowName);
220         if (!isValidInteger(/* action= */ "WAIT_MS Value", waitTime, workflowName)) {
221             throwRuntimeException("Wait", waitTime, workflowName, "Invalid");
222         }
223         spectatioUiUtil.waitNSeconds(Integer.parseInt(waitTime));
224     }
225 
validateAndExecuteCommand(String workflowName, SpectatioUiUtil spectatioUiUtil)226     private void validateAndExecuteCommand(String workflowName, SpectatioUiUtil spectatioUiUtil) {
227         String command = validateAndGetTaskConfigText(workflowName);
228         spectatioUiUtil.executeShellCommand(command);
229     }
230 
validateAndVerifyUiElement(String workflowName, SpectatioUiUtil spectatioUiUtil)231     private void validateAndVerifyUiElement(String workflowName, SpectatioUiUtil spectatioUiUtil) {
232         UiElement uiElement = validateAndGetTaskConfigUiElement(workflowName);
233         BySelector selector = uiElement.getBySelectorForUiElement();
234         if (!spectatioUiUtil.hasUiElement(selector)) {
235             throwRuntimeException(
236                     "UI Element", selector.toString(), workflowName, "Not in Foreground");
237         }
238     }
239 
validateAndVerifyPackage(String workflowName, SpectatioUiUtil spectatioUiUtil)240     private void validateAndVerifyPackage(String workflowName, SpectatioUiUtil spectatioUiUtil) {
241         String pkg = validateAndGetTaskConfigText(workflowName);
242         if (!spectatioUiUtil.hasPackageInForeground(pkg)) {
243             throwRuntimeException("Package", pkg, workflowName, "Not in Foreground");
244         }
245     }
246 
validateAndSwipe(String workflowName, SpectatioUiUtil spectatioUiUtil)247     private void validateAndSwipe(String workflowName, SpectatioUiUtil spectatioUiUtil) {
248         SwipeConfig swipeConfig = validateAndGetTaskSwipeConfig(workflowName);
249         SwipeDirection direction =
250                 validateAndGetEnumValue(
251                         workflowName,
252                         "Swipe Direction",
253                         SwipeDirection.class,
254                         swipeConfig.getSwipeDirection());
255         SwipeFraction fraction =
256                 validateAndGetEnumValue(
257                         workflowName,
258                         "Swipe Fraction",
259                         SwipeFraction.class,
260                         swipeConfig.getSwipeFraction());
261         spectatioUiUtil.swipe(direction, swipeConfig.getNumberOfSteps(), fraction);
262     }
263 
validateAndClickUiElement( String workflowName, SpectatioUiUtil spectatioUiUtil, FindType howToFind, boolean isLongClick, boolean isOptional)264     private void validateAndClickUiElement(
265             String workflowName,
266             SpectatioUiUtil spectatioUiUtil,
267             FindType howToFind,
268             boolean isLongClick,
269             boolean isOptional) {
270         UiElement uiElement = validateAndGetTaskConfigUiElement(workflowName);
271         BySelector selector = uiElement.getBySelectorForUiElement();
272         UiObject2 uiObject = spectatioUiUtil.findUiObject(selector);
273         if (howToFind == FindType.SCROLL && !isValidUiObject(uiObject)) {
274             ScrollConfig scrollConfig = validateAndGetTaskScrollConfig(workflowName);
275             ScrollActions scrollAction =
276                     validateAndGetEnumValue(
277                             workflowName,
278                             "Scroll Action",
279                             ScrollActions.class,
280                             scrollConfig.getScrollAction());
281             try {
282                 switch (scrollAction) {
283                     case USE_BUTTON:
284                         BySelector forwardButtonSelector =
285                                 scrollConfig.getScrollForwardButton().getBySelectorForUiElement();
286                         BySelector backwardButtonSelector =
287                                 scrollConfig.getScrollBackwardButton().getBySelectorForUiElement();
288                         uiObject =
289                                 spectatioUiUtil.scrollAndFindUiObject(
290                                         forwardButtonSelector, backwardButtonSelector, selector);
291                         break;
292                     case USE_GESTURE:
293                         BySelector scrollElementSelector =
294                                 scrollConfig.getScrollElement().getBySelectorForUiElement();
295                         Integer scrollMargin = Integer.valueOf(scrollConfig.getScrollMargin());
296                         Integer scrollWaitTime = Integer.valueOf(scrollConfig.getScrollWaitTime());
297 
298                         ScrollDirection scrollDirection =
299                                 validateAndGetEnumValue(
300                                         workflowName,
301                                         "Scroll Direction",
302                                         ScrollDirection.class,
303                                         scrollConfig.getScrollDirection());
304                         spectatioUiUtil.addScrollValues(scrollMargin, scrollWaitTime);
305                         uiObject =
306                                 spectatioUiUtil.scrollAndFindUiObject(
307                                         scrollElementSelector,
308                                         selector,
309                                         (scrollDirection == ScrollDirection.VERTICAL));
310                         break;
311                     default:
312                         throwRuntimeException(
313                                 "Scroll Action",
314                                 scrollConfig.getScrollAction(),
315                                 workflowName,
316                                 "Not Supported");
317                 }
318             } catch (MissingUiElementException ex) {
319                 throwRuntimeException(
320                         "Scroll Button or Element for Scroll Action",
321                         scrollConfig.getScrollAction(),
322                         workflowName,
323                         String.format("Missing. Error: %s", ex.getMessage()));
324             }
325         }
326         if (howToFind == FindType.SWIPE && !isValidUiObject(uiObject)) {
327             SwipeConfig swipeConfig = validateAndGetTaskSwipeConfig(workflowName);
328             SwipeDirection swipeDirection =
329                     validateAndGetEnumValue(
330                             workflowName,
331                             "Swipe Direction",
332                             SwipeDirection.class,
333                             swipeConfig.getSwipeDirection());
334             SwipeFraction swipeFraction =
335                     validateAndGetEnumValue(
336                             workflowName,
337                             "Swipe Fraction",
338                             SwipeFraction.class,
339                             swipeConfig.getSwipeFraction());
340             uiObject =
341                     spectatioUiUtil.swipeAndFindUiObject(
342                             swipeDirection,
343                             swipeConfig.getNumberOfSteps(),
344                             swipeFraction,
345                             selector);
346         }
347         if (isOptional && !isValidUiObject(uiObject)) {
348             return;
349         }
350         validateUiObject(uiObject, workflowName);
351         if (isLongClick) {
352             spectatioUiUtil.longPress(uiObject);
353         } else {
354             spectatioUiUtil.clickAndWait(uiObject);
355         }
356     }
357 
validateAndPressKey( String workflowName, SpectatioUiUtil spectatioUiUtil, boolean isLongPress)358     private void validateAndPressKey(
359             String workflowName, SpectatioUiUtil spectatioUiUtil, boolean isLongPress) {
360         String key = validateAndGetTaskConfigText(workflowName);
361         // Check if Key is an integer i.e. KeyCode
362         if (isValidInteger(/* action= */ "PRESS", key, workflowName)) {
363             int keyCode = Integer.parseInt(key);
364             if (isLongPress) {
365                 spectatioUiUtil.longPressKey(keyCode);
366             } else {
367                 spectatioUiUtil.pressKeyCode(keyCode);
368             }
369             return;
370         }
371         switch (key) {
372             case "POWER":
373                 if (isLongPress) {
374                     spectatioUiUtil.longPressPower();
375                 } else {
376                     spectatioUiUtil.pressPower();
377                 }
378                 break;
379             case "HOME":
380                 if (isLongPress) {
381                     throwRuntimeException("Long Press", key, workflowName, "Not Supported");
382                 } else {
383                     spectatioUiUtil.pressHome();
384                 }
385                 break;
386             case "BACK":
387                 if (isLongPress) {
388                     throwRuntimeException("Long Press", key, workflowName, "Not Supported");
389                 } else {
390                     spectatioUiUtil.pressBack();
391                 }
392                 break;
393             case "SCREEN_CENTER":
394                 if (isLongPress) {
395                     spectatioUiUtil.longPressScreenCenter();
396                 } else {
397                     throwRuntimeException("Press", key, workflowName, "Not Supported");
398                 }
399                 break;
400             case "WAKE_UP":
401                 if (isLongPress) {
402                     throwRuntimeException("Long Press", key, workflowName, "Not Supported");
403                 } else {
404                     spectatioUiUtil.wakeUp();
405                 }
406                 break;
407             default:
408                 throwRuntimeException("Config", key, workflowName, "Not Supported");
409         }
410     }
411 
isValidInteger(String action, String value, String workflowName)412     private boolean isValidInteger(String action, String value, String workflowName) {
413         try {
414             int intValue = Integer.parseInt(value);
415             if (intValue < 0) {
416                 throwRuntimeException(action, value, workflowName, "Invalid");
417             }
418         } catch (NumberFormatException ex) {
419             return false;
420         }
421         return true;
422     }
423 
isValidUiObject(UiObject2 uiObject)424     private boolean isValidUiObject(UiObject2 uiObject) {
425         return uiObject != null;
426     }
427 
validateUiObject(UiObject2 uiObject, String workflowName)428     private void validateUiObject(UiObject2 uiObject, String workflowName) {
429         if (!isValidUiObject(uiObject)) {
430             throwRuntimeException(
431                     "UI Element for Config", "UI_ELEMENT", workflowName, "Missing on Device UI");
432         }
433     }
434 
validateAndGetTaskConfigText(String workflowName)435     private String validateAndGetTaskConfigText(String workflowName) {
436         String taskConfigText = mTaskConfig.getText();
437         if (Strings.isNullOrEmpty(taskConfigText)) {
438             throwRuntimeException(
439                     "Config Text", taskConfigText, workflowName, "Missing or Invalid");
440         }
441         return taskConfigText.trim();
442     }
443 
validateAndGetTaskConfigUiElement(String workflowName)444     private UiElement validateAndGetTaskConfigUiElement(String workflowName) {
445         UiElement uiElement = mTaskConfig.getUiElement();
446         if (uiElement == null) {
447             throwRuntimeException("Config", "UI_ELEMENT", workflowName, "Missing or Invalid");
448         }
449         return uiElement;
450     }
451 
validateAndGetTaskScrollConfig(String workflowName)452     private ScrollConfig validateAndGetTaskScrollConfig(String workflowName) {
453         if (mScrollConfig == null) {
454             throwRuntimeException("Config", "SCROLL_CONFIG", workflowName, "Missing or Invalid");
455         }
456         return mScrollConfig;
457     }
458 
validateAndGetTaskSwipeConfig(String workflowName)459     private SwipeConfig validateAndGetTaskSwipeConfig(String workflowName) {
460         if (mSwipeConfig == null) {
461             throwRuntimeException("Config", "SWIPE_CONFIG", workflowName, "Missing or Invalid");
462         }
463         return mSwipeConfig;
464     }
465 
validateAndGetEnumValue( String workflowName, String property, Class<E> enumClass, String value)466     private <E extends Enum<E>> E validateAndGetEnumValue(
467             String workflowName, String property, Class<E> enumClass, String value) {
468         E enumValue = null;
469         try {
470             enumValue = Enum.valueOf(enumClass, value);
471         } catch (IllegalArgumentException ex) {
472             throwRuntimeException(property, value, workflowName, "Not Supported");
473         }
474         return enumValue;
475     }
476 
throwRuntimeException( String property, String value, String workflowName, String reason)477     private void throwRuntimeException(
478             String property, String value, String workflowName, String reason) {
479         throw new RuntimeException(
480                 String.format(
481                         "%s %s for task %s with type %s in Workflow %s is %s.",
482                         property, value, mName, mType, workflowName, reason));
483     }
484 }
485