1 /*
2  * Copyright (C) 2021 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.helpers;
18 
19 import android.app.Instrumentation;
20 import android.content.ActivityNotFoundException;
21 import android.os.SystemClock;
22 import android.util.Log;
23 
24 import android.graphics.Rect;
25 
26 import android.support.test.uiautomator.By;
27 import android.support.test.uiautomator.BySelector;
28 import android.support.test.uiautomator.Direction;
29 import android.support.test.uiautomator.UiDevice;
30 import android.support.test.uiautomator.UiObject2;
31 import android.support.test.uiautomator.Until;
32 
33 import java.io.IOException;
34 import java.util.List;
35 import java.util.regex.Pattern;
36 
37 public abstract class AbstractAutoStandardAppHelper extends AbstractStandardAppHelper {
38     private static final String LOG_TAG = AbstractAutoStandardAppHelper.class.getSimpleName();
39 
40     protected Instrumentation mInstrumentation;
41     protected UiDevice mDevice;
42 
43     private AutoJsonUtility mAutoJsonUtil;
44 
45     private static final int UI_RESPONSE_WAIT_MS = 5000;
46     private static final float DEFAULT_SCROLL_PERCENT = 100f;
47     private static final int DEFAULT_SCROLL_TIME_MS = 500;
48 
49     private static final int MAX_SCROLLS = 5;
50 
AbstractAutoStandardAppHelper(Instrumentation instrumentation)51     public AbstractAutoStandardAppHelper(Instrumentation instrumentation) {
52         super(instrumentation);
53         mInstrumentation = instrumentation;
54         mDevice = UiDevice.getInstance(instrumentation);
55         mAutoJsonUtil = AutoJsonUtility.getInstance();
56     }
57 
addConfigUtility(String appName, IAutoConfigUtility utility)58     protected void addConfigUtility(String appName, IAutoConfigUtility utility) {
59         mAutoJsonUtil.addConfigUtility(appName, utility);
60     }
61 
62     /** {@inheritDoc} */
63     @Override
open()64     public void open() {
65         // Launch the application as normal.
66         String pkg = getPackage();
67 
68         String output = null;
69         try {
70             Log.i(LOG_TAG, String.format("Sending command to launch: %s", pkg));
71             mInstrumentation.getContext().startActivity(getOpenAppIntent());
72         } catch (ActivityNotFoundException e) {
73             throw new RuntimeException(String.format("Failed to find package: %s", pkg), e);
74         }
75 
76         // Ensure the package is in the foreground for success.
77         if (!mDevice.wait(Until.hasObject(By.pkg(pkg).depth(0)), 30000)) {
78             throw new IllegalStateException(
79                     String.format("Did not find package, %s, in foreground.", pkg));
80         }
81     }
82 
83     /** {@inheritDoc} */
84     @Override
exit()85     public void exit() {
86         pressHome();
87         waitForIdle();
88     }
89 
90     /** {@inheritDoc} */
91     @Override
dismissInitialDialogs()92     public void dismissInitialDialogs() {
93         // Nothing to dismiss
94     }
95 
96     /** {@inheritDoc} */
97     @Override
getLauncherName()98     public String getLauncherName() {
99         throw new UnsupportedOperationException("Operation not supported.");
100     }
101 
102     /** {@inheritDoc} */
103     @Override
getPackage()104     public String getPackage() {
105         throw new UnsupportedOperationException("Operation not supported.");
106     }
107 
108     /**
109      * Executes a shell command on device, and return the standard output in string.
110      *
111      * @param command the command to run
112      * @return the standard output of the command, or empty string if failed without throwing an
113      *     IOException
114      */
executeShellCommand(String command)115     protected String executeShellCommand(String command) {
116         try {
117             return mDevice.executeShellCommand(command);
118         } catch (IOException e) {
119             // ignore
120             Log.e(
121                     LOG_TAG,
122                     String.format(
123                             "The shell command failed to run: %s exception: %s",
124                             command, e.getMessage()));
125             return "";
126         }
127     }
128 
129     /** Press Home Button on the Device */
pressHome()130     protected void pressHome() {
131         mDevice.pressHome();
132     }
133 
134     /** Press Back Button on the Device */
pressBack()135     protected void pressBack() {
136         mDevice.pressBack();
137     }
138 
139     /** Press Enter Button on the Device */
pressEnter()140     protected void pressEnter() {
141         mDevice.pressEnter();
142     }
143 
144     /** Press power button */
pressPowerButton()145     protected void pressPowerButton() {
146         executeShellCommand("input keyevent KEYCODE_POWER");
147         SystemClock.sleep(UI_RESPONSE_WAIT_MS);
148     }
149 
150     /** Wait for the device to be idle */
waitForIdle()151     protected void waitForIdle() {
152         mDevice.waitForIdle();
153     }
154 
155     /** Wait for the given selector to be gone */
waitForGone(BySelector selector)156     protected void waitForGone(BySelector selector) {
157         mDevice.wait(Until.gone(selector), UI_RESPONSE_WAIT_MS);
158     }
159 
160     /** Wait for window change on the device */
waitForWindowUpdate(String applicationPackage)161     protected void waitForWindowUpdate(String applicationPackage) {
162         mDevice.waitForWindowUpdate(applicationPackage, UI_RESPONSE_WAIT_MS);
163     }
164 
165     /**
166      * Scroll in given direction by specified percent of the whole scrollable region in given time.
167      *
168      * @param direction The direction in which to perform scrolling, it's either up or down.
169      * @param percent The percentage of the whole scrollable region by which to scroll, ranging from
170      *     0 - 100. For instance, percent = 50 would scroll up/down by half of the screen.
171      * @param timeMs The duration in milliseconds to perform the scrolling gesture.
172      * @param index Index required for split screen.
173      */
scroll(Direction direction, float percent, long timeMs, int index)174     private boolean scroll(Direction direction, float percent, long timeMs, int index) {
175         boolean canScrollMoreInGivenDircetion = false;
176         List<UiObject2> upButtons =
177                 findUiObjects(
178                         getResourceFromConfig(
179                                 AutoConfigConstants.SETTINGS,
180                                 AutoConfigConstants.FULL_SETTINGS,
181                                 AutoConfigConstants.UP_BUTTON));
182         List<UiObject2> downButtons =
183                 findUiObjects(
184                         getResourceFromConfig(
185                                 AutoConfigConstants.SETTINGS,
186                                 AutoConfigConstants.FULL_SETTINGS,
187                                 AutoConfigConstants.DOWN_BUTTON));
188         List<UiObject2> scrollableObjects = findUiObjects(By.scrollable(true));
189         if (scrollableObjects == null || upButtons == null || scrollableObjects.size() == 0) {
190             return canScrollMoreInGivenDircetion;
191         }
192         if (upButtons.size() == 1 || (scrollableObjects.size() - 1) < index) {
193             // reset index as it is invalid
194             index = 0;
195         }
196         if (upButtons != null) {
197             UiObject2 upButton = upButtons.get(index);
198             UiObject2 downButton = downButtons.get(index);
199             if (direction == Direction.UP) {
200                 clickAndWaitForIdleScreen(upButton);
201             } else if (direction == Direction.DOWN) {
202                 clickAndWaitForIdleScreen(downButton);
203             }
204         } else {
205             UiObject2 scrollable = scrollableObjects.get(index);
206             if (scrollable != null) {
207                 scrollable.setGestureMargins(
208                         getScrollableMargin(scrollable, false), // left
209                         getScrollableMargin(scrollable, true), // top
210                         getScrollableMargin(scrollable, false), // right
211                         getScrollableMargin(scrollable, true)); // bottom
212                 int scrollSpeed = getScrollSpeed(scrollable, timeMs);
213                 canScrollMoreInGivenDircetion =
214                         scrollable.scroll(direction, percent / 100, scrollSpeed);
215             }
216         }
217         return canScrollMoreInGivenDircetion;
218     }
219 
220     /**
221      * Return the margin for scrolling.
222      *
223      * @param scrollable The given scrollable object to scroll through.
224      * @param isVertical If true, then vertical else horizontal
225      */
getScrollableMargin(UiObject2 scrollable, boolean isVertical)226     private int getScrollableMargin(UiObject2 scrollable, boolean isVertical) {
227         Rect bounds = scrollable.getVisibleBounds();
228         int margin = (int) (Math.abs(bounds.width()) / 4);
229         if (isVertical) {
230             margin = (int) (Math.abs(bounds.height()) / 4);
231         }
232         return margin;
233     }
234 
235     /**
236      * Return the scroll speed such that it takes given time for the device to scroll through the
237      * whole scrollable region(i.e. from the top of the scrollable region to bottom).
238      *
239      * @param scrollable The given scrollable object to scroll through.
240      * @param timeMs The duration in milliseconds to perform the scrolling gesture.
241      */
getScrollSpeed(UiObject2 scrollable, long timeMs)242     private int getScrollSpeed(UiObject2 scrollable, long timeMs) {
243         Rect bounds = scrollable.getVisibleBounds();
244         double timeSeconds = (double) timeMs / 1000;
245         int scrollSpeed = (int) (bounds.height() / timeSeconds);
246         return scrollSpeed;
247     }
248 
249     /**
250      * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by
251      * one page).
252      */
scrollDownOnePage()253     public boolean scrollDownOnePage() {
254         return scrollDownOnePage(0);
255     }
256 
257     /**
258      * Scroll down from the top of the scrollable region to bottom of the scrollable region (i.e. by
259      * one page). Index - required for split screen
260      */
scrollDownOnePage(int index)261     protected boolean scrollDownOnePage(int index) {
262         return scroll(Direction.DOWN, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index);
263     }
264 
265     /**
266      * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by
267      * one page).
268      */
scrollUpOnePage()269     public boolean scrollUpOnePage() {
270         return scrollUpOnePage(0);
271     }
272 
273     /**
274      * Scroll up from the bottom of the scrollable region to top of the scrollable region (i.e. by
275      * one page). Index - required for split screen
276      */
scrollUpOnePage(int index)277     protected boolean scrollUpOnePage(int index) {
278         return scroll(Direction.UP, DEFAULT_SCROLL_PERCENT, DEFAULT_SCROLL_TIME_MS, index);
279     }
280 
281     /** Find UI Object in a Scrollable view */
scrollAndFindUiObject(BySelector selector)282     protected UiObject2 scrollAndFindUiObject(BySelector selector) {
283         return scrollAndFindUiObject(selector, 0);
284     }
285 
286     /** Find UI Object in a Scrollable view with Index ( required for split screen ) */
scrollAndFindUiObject(BySelector selector, int index)287     protected UiObject2 scrollAndFindUiObject(BySelector selector, int index) {
288         if (selector == null) {
289             return null;
290         }
291         // Find the object on current page
292         UiObject2 uiObject = findUiObject(selector);
293         if (uiObject != null) {
294             return uiObject;
295         }
296         // Scroll To Top
297         scrollToTop(index);
298         // Find UI Object on the first page
299         uiObject = findUiObject(selector);
300         // Try finding UI Object until it's found or all the pages are checked
301         int scrollCount = 0;
302         while (uiObject == null && scrollCount < MAX_SCROLLS) {
303             // Scroll down to next page
304             scrollDownOnePage(index);
305 
306             // Find UI Object
307             uiObject = findUiObject(selector);
308 
309             scrollCount++;
310         }
311         return uiObject;
312     }
313 
314     /** Scroll to top of the scrollable region. */
scrollToTop()315     protected void scrollToTop() {
316         scrollToTop(0);
317     }
318 
319     /** Scroll to top of the scrollable region with index. ( Required for Split Screen ) */
scrollToTop(int index)320     protected void scrollToTop(int index) {
321         int scrollCount = 0;
322         while (scrollCount < MAX_SCROLLS) {
323             scrollUpOnePage(index);
324             scrollCount++;
325         }
326     }
327 
328     /** Find the UI Object that matches the given selector */
findUiObject(BySelector selector)329     protected UiObject2 findUiObject(BySelector selector) {
330         if (selector == null) {
331             return null;
332         }
333         UiObject2 uiObject = mDevice.wait(Until.findObject(selector), UI_RESPONSE_WAIT_MS);
334         return uiObject;
335     }
336 
337     /** Find the list of UI object that matches the given selector */
findUiObjects(BySelector selector)338     protected List<UiObject2> findUiObjects(BySelector selector) {
339         if (selector == null) {
340             return null;
341         }
342         List<UiObject2> uiObjects = mDevice.wait(Until.findObjects(selector), UI_RESPONSE_WAIT_MS);
343         return uiObjects;
344     }
345 
346     /** Find the list of UI object that matches the given selector for given depth */
findUiObjects(BySelector selector, int depth)347     protected List<UiObject2> findUiObjects(BySelector selector, int depth) {
348         if (selector == null) {
349             return null;
350         }
351         List<UiObject2> uiObjects =
352                 mDevice.wait(Until.findObjects(selector.maxDepth(depth)), UI_RESPONSE_WAIT_MS);
353         return uiObjects;
354     }
355 
356     /**
357      * This method is used to click on an UiObject2 and wait for device idle after click.
358      *
359      * @param uiObject UiObject2 to click.
360      */
clickAndWaitForIdleScreen(UiObject2 uiObject2)361     protected void clickAndWaitForIdleScreen(UiObject2 uiObject2) {
362         uiObject2.click();
363         waitForIdle();
364     }
365 
366     /**
367      * This method is used to click on an UiObject2 and wait for window update.
368      *
369      * @param appPackage application package for window update
370      * @param uiObject2 UiObject2 to click.
371      */
clickAndWaitForWindowUpdate(String appPackage, UiObject2 uiObject2)372     protected void clickAndWaitForWindowUpdate(String appPackage, UiObject2 uiObject2) {
373         uiObject2.click();
374         waitForWindowUpdate(appPackage);
375         waitForIdle();
376     }
377 
378     /**
379      * This method is used to click on an UiObject2 and wait until it is gone
380      *
381      * @param uiObject2 uiObject to be clicked
382      * @param selector BySelector to be gone
383      */
clickAndWaitForGone(UiObject2 uiObject2, BySelector selector)384     protected void clickAndWaitForGone(UiObject2 uiObject2, BySelector selector) {
385         uiObject2.click();
386         waitForGone(selector);
387     }
388 
389     /**
390      * This method is used check if given object is visible on the device screen
391      *
392      * @param selector BySelector to be gone
393      */
hasUiObject(BySelector selector)394     protected boolean hasUiObject(BySelector selector) {
395         return mDevice.hasObject(selector);
396     }
397 
398     /** Get path for the given setting */
getSettingPath(String setting)399     protected String[] getSettingPath(String setting) {
400         return mAutoJsonUtil.getSettingPath(setting);
401     }
402 
403     /** Get available options for given settings */
getSettingOptions(String setting)404     protected String[] getSettingOptions(String setting) {
405         return mAutoJsonUtil.getSettingOptions(setting);
406     }
407 
408     /** Get application config value for given configuration */
getApplicationConfig(String config)409     protected String getApplicationConfig(String config) {
410         return mAutoJsonUtil.getApplicationConfig(config);
411     }
412 
413     /** Get resource for given configuration resource in given application */
getResourceFromConfig( String appName, String appConfig, String appResource)414     protected BySelector getResourceFromConfig(
415             String appName, String appConfig, String appResource) {
416         AutoConfigResource configResource =
417                 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
418 
419         // RESOURCE_ID
420         if (configResource != null
421                 && AutoConfigConstants.RESOURCE_ID.equals(configResource.getResourceType())) {
422             return By.res(configResource.getResourcePackage(), configResource.getResourceValue());
423         }
424 
425         // TEXT
426         if (configResource != null
427                 && AutoConfigConstants.TEXT.equals(configResource.getResourceType())) {
428             return By.text(
429                     Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE));
430         }
431 
432         // TEXT_CONTAINS
433         if (configResource != null
434                 && AutoConfigConstants.TEXT_CONTAINS.equals(configResource.getResourceType())) {
435             return By.textContains(configResource.getResourceValue());
436         }
437 
438         // DESCRIPTION
439         if (configResource != null
440                 && AutoConfigConstants.DESCRIPTION.equals(configResource.getResourceType())) {
441             return By.desc(
442                     Pattern.compile(configResource.getResourceValue(), Pattern.CASE_INSENSITIVE));
443         }
444 
445         // CLASS
446         if (configResource != null
447                 && AutoConfigConstants.CLASS.equals(configResource.getResourceType())) {
448             if (configResource.getResourcePackage() != null
449                     && !configResource.getResourcePackage().isEmpty()) {
450                 return By.clazz(
451                         configResource.getResourcePackage(), configResource.getResourceValue());
452             }
453             return By.clazz(configResource.getResourceValue());
454         }
455 
456         return null;
457     }
458 
459     /** Get resource value for given configuration resource in given application */
getResourceValue(String appName, String appConfig, String appResource)460     protected String getResourceValue(String appName, String appConfig, String appResource) {
461         AutoConfigResource configResource =
462                 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
463 
464         if (configResource != null) {
465             return configResource.getResourceValue();
466         }
467 
468         return null;
469     }
470 
471     /** Get resource package for given configuration resource in given application */
getResourcePackage(String appName, String appConfig, String appResource)472     protected String getResourcePackage(String appName, String appConfig, String appResource) {
473         AutoConfigResource configResource =
474                 mAutoJsonUtil.getResourceFromConfig(appName, appConfig, appResource);
475 
476         if (configResource != null) {
477             return configResource.getResourcePackage();
478         }
479 
480         return null;
481     }
482 
483     /** Check for Split Screen UI in Settings Application */
hasSplitScreenSettingsUI()484     protected boolean hasSplitScreenSettingsUI() {
485         boolean isSplitScreen = false;
486         if ("TRUE"
487                 .equalsIgnoreCase(
488                         mAutoJsonUtil.getApplicationConfig(AutoConfigConstants.SPLIT_SCREEN_UI))) {
489             isSplitScreen = true;
490         }
491         return isSplitScreen;
492     }
493 }
494