• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.utils;
18 
19 import android.app.Instrumentation;
20 import android.graphics.Point;
21 import android.graphics.Rect;
22 import android.os.RemoteException;
23 import android.os.SystemClock;
24 import android.platform.spectatio.constants.JsonConfigConstants;
25 import android.platform.spectatio.exceptions.MissingUiElementException;
26 import android.util.Log;
27 import android.view.KeyEvent;
28 
29 import androidx.test.uiautomator.By;
30 import androidx.test.uiautomator.BySelector;
31 import androidx.test.uiautomator.Direction;
32 import androidx.test.uiautomator.UiDevice;
33 import androidx.test.uiautomator.UiObject2;
34 import androidx.test.uiautomator.Until;
35 
36 import com.google.common.base.Strings;
37 import com.google.escapevelocity.Template;
38 
39 import java.io.ByteArrayInputStream;
40 import java.io.ByteArrayOutputStream;
41 import java.io.IOException;
42 import java.io.InputStreamReader;
43 import java.nio.charset.StandardCharsets;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.Map;
49 import java.util.Set;
50 
51 public class SpectatioUiUtil {
52     private static final String LOG_TAG = SpectatioUiUtil.class.getSimpleName();
53 
54     private static SpectatioUiUtil sSpectatioUiUtil = null;
55 
56     private static final int SHORT_UI_RESPONSE_WAIT_MS = 1000;
57     private static final int LONG_UI_RESPONSE_WAIT_MS = 5000;
58     private static final int TEN_SECONDS_WAIT = 10000;
59     private static final int EXTRA_LONG_UI_RESPONSE_WAIT_MS = 15000;
60     private static final int LONG_PRESS_DURATION_MS = 5000;
61     private static final int MAX_SCROLL_COUNT = 100;
62     private static final int MAX_SWIPE_STEPS = 10;
63     private static final float SCROLL_PERCENT = 1.0f;
64     private static final float SWIPE_PERCENT = 1.0f;
65 
66     private int mWaitTimeAfterScroll = 5; // seconds
67     private int mScrollMargin = 4;
68 
69     private UiDevice mDevice;
70 
71     public enum SwipeDirection {
72         TOP_TO_BOTTOM,
73         BOTTOM_TO_TOP,
74         LEFT_TO_RIGHT,
75         RIGHT_TO_LEFT
76     }
77 
78     /**
79      * Defines the swipe fraction, allowing for a swipe to be performed from a 5-pad distance, a
80      * quarter, half, three-quarters of the screen, or the full screen.
81      *
82      * <p>DEFAULT: Swipe from one side of the screen to another side, with a 5-pad distance from the
83      * edge.
84      *
85      * <p>QUARTER: Swipe from one side, a quarter of the distance of the entire screen away from the
86      * edge, to the other side.
87      *
88      * <p>HALF: Swipe from the center of the screen to the other side.
89      *
90      * <p>THREEQUARTER: Swipe from one side, three-quarters of the distance of the entire screen
91      * away from the edge, to the other side.
92      *
93      * <p>FULL: Swipe from one edge of the screen to the other edge.
94      */
95     public enum SwipeFraction {
96         DEFAULT,
97         QUARTER,
98         HALF,
99         THREEQUARTER,
100         FULL,
101     }
102 
103     /**
104      * Defines the swipe speed based on the number of steps.
105      *
106      * <p><a
107      * href="https://developer.android.com/reference/androidx/test/uiautomator/UiDevice#swipe(int,int,int,int,int)">UiDevie#Swipe</a>
108      * performs a swipe from one coordinate to another using the number of steps to determine
109      * smoothness and speed. Each step execution is throttled to 5ms per step. So for a 100 steps,
110      * the swipe will take about 1/2 second to complete.
111      */
112     public enum SwipeSpeed {
113         NORMAL(200), // equals to 1000ms in duration.
114         SLOW(1000), // equals to 5000ms in duration.
115         FAST(50), // equals to 250ms in duration.
116         FLING(20); // equals to 100ms in duration.
117 
118         final int mNumSteps;
119 
SwipeSpeed(int numOfSteps)120         SwipeSpeed(int numOfSteps) {
121             this.mNumSteps = numOfSteps;
122         }
123     }
124 
SpectatioUiUtil(UiDevice mDevice)125     private SpectatioUiUtil(UiDevice mDevice) {
126         this.mDevice = mDevice;
127     }
128 
getInstance(UiDevice mDevice)129     public static SpectatioUiUtil getInstance(UiDevice mDevice) {
130         if (sSpectatioUiUtil == null) {
131             sSpectatioUiUtil = new SpectatioUiUtil(mDevice);
132         }
133         return sSpectatioUiUtil;
134     }
135 
136     /**
137      * Initialize a UiDevice for the given instrumentation, then initialize Spectatio for that
138      * device. If Spectatio has already been initialized, return the previously initialized
139      * instance.
140      */
getInstance(Instrumentation instrumentation)141     public static SpectatioUiUtil getInstance(Instrumentation instrumentation) {
142         return getInstance(UiDevice.getInstance(instrumentation));
143     }
144 
145     /** Sets the scroll margin and wait time after the scroll */
addScrollValues(Integer scrollMargin, Integer waitTime)146     public void addScrollValues(Integer scrollMargin, Integer waitTime) {
147         this.mScrollMargin = scrollMargin;
148         this.mWaitTimeAfterScroll = waitTime;
149     }
150 
pressBack()151     public boolean pressBack() {
152         return mDevice.pressBack();
153     }
154 
pressHome()155     public boolean pressHome() {
156         return mDevice.pressHome();
157     }
158 
pressKeyCode(int keyCode)159     public boolean pressKeyCode(int keyCode) {
160         return mDevice.pressKeyCode(keyCode);
161     }
162 
pressPower()163     public boolean pressPower() {
164         return pressKeyCode(KeyEvent.KEYCODE_POWER);
165     }
166 
longPress(UiObject2 uiObject)167     public boolean longPress(UiObject2 uiObject) {
168         if (!isValidUiObject(uiObject)) {
169             Log.e(
170                     LOG_TAG,
171                     "Cannot Long Press UI Object; Provide a valid UI Object, currently it is"
172                             + " NULL.");
173             return false;
174         }
175         if (!uiObject.isLongClickable()) {
176             Log.e(
177                     LOG_TAG,
178                     "Cannot Long Press UI Object; Provide a valid UI Object, "
179                             + "current UI Object is not long clickable.");
180             return false;
181         }
182         uiObject.longClick();
183         wait1Second();
184         return true;
185     }
186 
longPressKey(int keyCode)187     public boolean longPressKey(int keyCode) {
188         try {
189             // Use English Locale because ADB Shell command does not depend on Device UI
190             mDevice.executeShellCommand(
191                     String.format(Locale.ENGLISH, "input keyevent --longpress %d", keyCode));
192             wait1Second();
193             return true;
194         } catch (IOException e) {
195             // Ignore
196             Log.e(
197                     LOG_TAG,
198                     String.format(
199                             "Failed to long press key code: %d, Error: %s",
200                             keyCode, e.getMessage()));
201         }
202         return false;
203     }
204 
longPressPower()205     public boolean longPressPower() {
206         return longPressKey(KeyEvent.KEYCODE_POWER);
207     }
208 
longPressScreenCenter()209     public boolean longPressScreenCenter() {
210         Rect bounds = getScreenBounds();
211         int xCenter = bounds.centerX();
212         int yCenter = bounds.centerY();
213         try {
214             // Click method in UiDevice only takes x and y co-ordintes to tap,
215             // so it can be clicked but cannot be pressed for long time
216             // Use ADB command to Swipe instead (because UiDevice swipe method don't take duration)
217             // i.e. simulate long press by swiping from
218             // center of screen to center of screen (i.e. same points) for long duration
219             // Use English Locale because ADB Shell command does not depend on Device UI
220             mDevice.executeShellCommand(
221                     String.format(
222                             Locale.ENGLISH,
223                             "input swipe %d %d %d %d %d",
224                             xCenter,
225                             yCenter,
226                             xCenter,
227                             yCenter,
228                             LONG_PRESS_DURATION_MS));
229             wait1Second();
230             return true;
231         } catch (IOException e) {
232             // Ignore
233             Log.e(
234                     LOG_TAG,
235                     String.format(
236                             "Failed to long press on screen center. Error: %s", e.getMessage()));
237         }
238         return false;
239     }
240 
wakeUp()241     public void wakeUp() {
242         try {
243             mDevice.wakeUp();
244         } catch (RemoteException ex) {
245             throw new IllegalStateException("Failed to wake up device.", ex);
246         }
247     }
248 
clickAndWait(UiObject2 uiObject)249     public void clickAndWait(UiObject2 uiObject) {
250         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Click");
251         uiObject.click();
252         wait1Second();
253     }
254 
clickAndWait(UiObject2 uiObject, int waitTime)255     public void clickAndWait(UiObject2 uiObject, int waitTime) {
256         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Click");
257         uiObject.click();
258         waitNSeconds(waitTime);
259     }
260 
261     /**
262      * Click and wait until new window opens, wait for the specified time. Usecases * Clicking a
263      * object that opens a new window * Navigating between screens * Clicking an object that can
264      * open a popup
265      */
clickAndWaitUntilNewWindowAppears(UiObject2 uiObject)266     public void clickAndWaitUntilNewWindowAppears(UiObject2 uiObject) {
267         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Click");
268         uiObject.clickAndWait(Until.newWindow(), EXTRA_LONG_UI_RESPONSE_WAIT_MS);
269     }
270 
271     /**
272      * Click at a specific location in the UI, and wait one second
273      *
274      * @param location Where to click
275      */
clickAndWait(Point location)276     public void clickAndWait(Point location) {
277         mDevice.click(location.x, location.y);
278         wait1Second();
279     }
280 
waitForIdle()281     public void waitForIdle() {
282         mDevice.waitForIdle();
283     }
284 
wait1Second()285     public void wait1Second() {
286         waitNSeconds(SHORT_UI_RESPONSE_WAIT_MS);
287     }
288 
wait5Seconds()289     public void wait5Seconds() {
290         waitNSeconds(LONG_UI_RESPONSE_WAIT_MS);
291     }
292 
293     /** Waits for 15 seconds */
wait15Seconds()294     public void wait15Seconds() {
295         waitNSeconds(EXTRA_LONG_UI_RESPONSE_WAIT_MS);
296     }
297 
waitNSeconds(int waitTime)298     public void waitNSeconds(int waitTime) {
299         SystemClock.sleep(waitTime);
300     }
301 
302     /**
303      * Executes a shell command on device, and return the standard output in string.
304      *
305      * @param command the command to run
306      * @return the standard output of the command, or empty string if failed without throwing an
307      *     IOException
308      */
executeShellCommand(String command)309     public String executeShellCommand(String command) {
310         validateText(command, /* type= */ "Command");
311         String populatedCommand = populateShellCommand(command);
312         Log.d(
313                 LOG_TAG,
314                 String.format(
315                         "Initial command: %s. Populated command: %s",
316                         command, populatedCommand));
317         try {
318             return mDevice.executeShellCommand(populatedCommand);
319         } catch (IOException e) {
320             // ignore
321             Log.e(
322                     LOG_TAG,
323                     String.format(
324                             "The shell command failed to run: %s, Error: %s",
325                             populatedCommand, e.getMessage()));
326             return "";
327         }
328     }
329 
populateShellCommand(String command)330     private String populateShellCommand(String command) {
331         String populatedCommand = command;
332 
333         // Map of supported substitutions
334         Map<String, String> vars = new HashMap<>();
335         try {
336             vars.put("user_id", mDevice.executeShellCommand("am get-current-user"));
337         } catch (IOException e) {
338             Log.e(LOG_TAG, "Could not execute `am get-current-user` to retrieve user id");
339         }
340 
341         try (InputStreamReader reader =
342                 new InputStreamReader(
343                         new ByteArrayInputStream(command.getBytes(StandardCharsets.UTF_8)))) {
344             Template template = Template.parseFrom(reader);
345             populatedCommand = template.evaluate(vars);
346             Log.d(
347                     LOG_TAG,
348                     String.format(
349                             "Initial command: %s. Populated command: %s",
350                             command, populatedCommand));
351         } catch (IOException e) {
352             Log.e(
353                     LOG_TAG,
354                     String.format(
355                             "Error populating the shell command template %s, Error: %s",
356                             command, e.getMessage()));
357         }
358         return populatedCommand;
359     }
360 
361     /** Find and return the UI Object that matches the given selector */
findUiObject(BySelector selector)362     public UiObject2 findUiObject(BySelector selector) {
363         validateSelector(selector, /* action= */ "Find UI Object");
364         UiObject2 uiObject = mDevice.wait(Until.findObject(selector), LONG_UI_RESPONSE_WAIT_MS);
365         return uiObject;
366     }
367 
368     /** Find and return the UI Objects that matches the given selector */
findUiObjects(BySelector selector)369     public List<UiObject2> findUiObjects(BySelector selector) {
370         validateSelector(selector, /* action= */ "Find UI Object");
371         List<UiObject2> uiObjects =
372                 mDevice.wait(Until.findObjects(selector), LONG_UI_RESPONSE_WAIT_MS);
373         return uiObjects;
374     }
375 
376     /**
377      * Find the UI Object that matches the given text string.
378      *
379      * @param text Text to search on device UI. It should exactly match the text visible on UI.
380      */
findUiObject(String text)381     public UiObject2 findUiObject(String text) {
382         validateText(text, /* type= */ "Text");
383         return findUiObject(By.text(text));
384     }
385 
386     /**
387      * Find the UI Object in given element.
388      *
389      * @param uiObject Find the ui object(selector) in this element.
390      * @param selector Find this ui object in the given element.
391      */
findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector)392     public UiObject2 findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector) {
393         validateUiObjectAndThrowIllegalArgumentException(
394                 uiObject, /* action= */ "Find UI object in given element");
395         validateSelector(selector, /* action= */ "Find UI object in given element");
396         return uiObject.findObject(selector);
397     }
398 
399     /**
400      * Waits for a UI element to appear within a specified timeout.
401      * Relpacement of findUiObject().
402      * Reference: https://developer.android.com/reference/androidx/test/uiautomator/UiDevice#wait
403      *
404      * @param selector The BySelector used to locate the element.
405      * @param timeout  The maximum time to wait in milliseconds.
406      * @return The UiObject2 representing the found element, or null if it's not found within the timeout.
407      */
waitForUiObject(BySelector selector, int timeout)408     public UiObject2 waitForUiObject(BySelector selector, int timeout) {
409         Log.i(LOG_TAG, "Waiting for UI element: " + selector);
410         validateSelector(selector, /* action= */ "Find UI Object");
411 
412         UiObject2 uiObject = mDevice.wait(Until.findObject(selector), timeout);
413         if (uiObject == null) {
414             Log.w(LOG_TAG, "UI element not found within timeout: " + selector);
415         }
416 
417         return uiObject;
418     }
419 
420     /**
421      * Waits for a UI element to appear using the default timeout.
422      *
423      * @param selector The BySelector used to locate the element.
424      * @return The UiObject2 representing the found element, or null if it's not found within the default timeout.
425      */
waitForUiObject(BySelector selector)426     public UiObject2 waitForUiObject(BySelector selector) {
427         return waitForUiObject(selector, TEN_SECONDS_WAIT);
428     }
429 
430     /**
431      * Checks if given text is available on the Device UI. The text should be exactly same as seen
432      * on the screen.
433      *
434      * <p>Given text will be searched on current screen. This method will not scroll on the screen
435      * to check for given text.
436      *
437      * @param text Text to search on device UI
438      * @return Returns True if the text is found, else return False.
439      */
hasUiElement(String text)440     public boolean hasUiElement(String text) {
441         validateText(text, /* type= */ "Text");
442         return hasUiElement(By.text(text));
443     }
444 
445     /**
446      * Scroll using forward and backward buttons on device screen and check if the given text is
447      * present.
448      *
449      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
450      * available on the Device UI.
451      *
452      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
453      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
454      * @param text Text to search on device UI
455      * @return Returns True if the text is found, else return False.
456      */
scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, String text)457     public boolean scrollAndCheckIfUiElementExist(
458             BySelector forward, BySelector backward, String text) throws MissingUiElementException {
459         return scrollAndFindUiObject(forward, backward, text) != null;
460     }
461 
462     /**
463      * Scroll by performing forward and backward gestures on device screen and check if the given
464      * text is present on Device UI.
465      *
466      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
467      * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text, boolean
468      * isVertical)} by passing isVertical = false.
469      *
470      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
471      * available on the Device UI.
472      *
473      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
474      * @param text Text to search on device UI
475      * @return Returns True if the text is found, else return False.
476      */
scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text)477     public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text)
478             throws MissingUiElementException {
479         return scrollAndCheckIfUiElementExist(scrollableSelector, text, /* isVertical= */ true);
480     }
481 
482     /**
483      * Scroll by performing forward and backward gestures on device screen and check if the given
484      * text is present on Device UI.
485      *
486      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
487      * available on the Device UI.
488      *
489      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
490      * @param text Text to search on device UI
491      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
492      *     use isVertical = false.
493      * @return Returns True if the text is found, else return False.
494      */
scrollAndCheckIfUiElementExist( BySelector scrollableSelector, String text, boolean isVertical)495     public boolean scrollAndCheckIfUiElementExist(
496             BySelector scrollableSelector, String text, boolean isVertical)
497             throws MissingUiElementException {
498         return scrollAndFindUiObject(scrollableSelector, text, isVertical) != null;
499     }
500 
501     /**
502      * Checks if given target is available on the Device UI.
503      *
504      * <p>Given target will be searched on current screen. This method will not scroll on the screen
505      * to check for given target.
506      *
507      * @param target {@link BySelector} to search on device UI
508      * @return Returns True if the target is found, else return False.
509      */
hasUiElement(BySelector target)510     public boolean hasUiElement(BySelector target) {
511         validateSelector(target, /* action= */ "Check For UI Object");
512         return mDevice.hasObject(target);
513     }
514 
515     /**
516      * Scroll using forward and backward buttons on device screen and check if the given target is
517      * present.
518      *
519      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
520      * available on the Device UI.
521      *
522      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
523      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
524      * @param target {@link BySelector} to search on device UI
525      * @return Returns True if the target is found, else return False.
526      */
scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, BySelector target)527     public boolean scrollAndCheckIfUiElementExist(
528             BySelector forward, BySelector backward, BySelector target)
529             throws MissingUiElementException {
530         return scrollAndFindUiObject(forward, backward, target) != null;
531     }
532 
533     /**
534      * Scroll by performing forward and backward gestures on device screen and check if the target
535      * UI Element is present.
536      *
537      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
538      * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target, boolean
539      * isVertical)} by passing isVertical = false.
540      *
541      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
542      * available on the Device UI.
543      *
544      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
545      * @param target {@link BySelector} to search on device UI
546      * @return Returns True if the target is found, else return False.
547      */
scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target)548     public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target)
549             throws MissingUiElementException {
550         return scrollAndCheckIfUiElementExist(scrollableSelector, target, /* isVertical= */ true);
551     }
552 
553     /**
554      * Scroll by performing forward and backward gestures on device screen and check if the target
555      * UI Element is present.
556      *
557      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
558      * available on the Device UI.
559      *
560      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
561      * @param target {@link BySelector} to search on device UI
562      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
563      *     use isVertical = false.
564      * @return Returns True if the target is found, else return False.
565      */
scrollAndCheckIfUiElementExist( BySelector scrollableSelector, BySelector target, boolean isVertical)566     public boolean scrollAndCheckIfUiElementExist(
567             BySelector scrollableSelector, BySelector target, boolean isVertical)
568             throws MissingUiElementException {
569         return scrollAndFindUiObject(scrollableSelector, target, isVertical) != null;
570     }
571 
hasPackageInForeground(String packageName)572     public boolean hasPackageInForeground(String packageName) {
573         validateText(packageName, /* type= */ "Package");
574         return mDevice.hasObject(By.pkg(packageName).depth(0));
575     }
576 
577     /** Click at the specified location on the device */
click(int x, int y)578     public void click(int x, int y) throws IOException {
579         mDevice.click(x, y);
580     }
581 
swipeUp()582     public void swipeUp() {
583         // Swipe Up From bottom of screen to the top in one step
584         swipe(SwipeDirection.BOTTOM_TO_TOP, /*numOfSteps*/ MAX_SWIPE_STEPS);
585     }
586 
swipeDown()587     public void swipeDown() {
588         // Swipe Down From top of screen to the bottom in one step
589         swipe(SwipeDirection.TOP_TO_BOTTOM, /*numOfSteps*/ MAX_SWIPE_STEPS);
590     }
591 
swipeRight()592     public void swipeRight() {
593         // Swipe Right From left of screen to the right in one step
594         swipe(SwipeDirection.LEFT_TO_RIGHT, /*numOfSteps*/ MAX_SWIPE_STEPS);
595     }
596 
swipeLeft()597     public void swipeLeft() {
598         // Swipe Left From right of screen to the left in one step
599         swipe(SwipeDirection.RIGHT_TO_LEFT, /*numOfSteps*/ MAX_SWIPE_STEPS);
600     }
601 
swipe(SwipeDirection swipeDirection, int numOfSteps)602     public void swipe(SwipeDirection swipeDirection, int numOfSteps) {
603         swipe(swipeDirection, numOfSteps, SwipeFraction.DEFAULT);
604     }
605 
606     /**
607      * Perform a swipe gesture
608      *
609      * @param swipeDirection The direction to perform the swipe in
610      * @param numOfSteps How many steps the swipe will take
611      * @param swipeFraction The fraction of the screen to swipe across
612      */
swipe(SwipeDirection swipeDirection, int numOfSteps, SwipeFraction swipeFraction)613     public void swipe(SwipeDirection swipeDirection, int numOfSteps, SwipeFraction swipeFraction) {
614         Rect bounds = getScreenBounds();
615 
616         List<Point> swipePoints = getPointsToSwipe(bounds, swipeDirection, swipeFraction);
617 
618         Point startPoint = swipePoints.get(0);
619         Point finishPoint = swipePoints.get(1);
620 
621         // Swipe from start pont to finish point in given number of steps
622         mDevice.swipe(startPoint.x, startPoint.y, finishPoint.x, finishPoint.y, numOfSteps);
623     }
624 
625     /**
626      * Perform a swipe gesture
627      *
628      * @param swipeDirection The direction to perform the swipe in
629      * @param swipeSpeed How fast to swipe
630      */
swipe(SwipeDirection swipeDirection, SwipeSpeed swipeSpeed)631     public void swipe(SwipeDirection swipeDirection, SwipeSpeed swipeSpeed) throws IOException {
632         swipe(swipeDirection, swipeSpeed.mNumSteps);
633     }
634 
635     /**
636      * Perform a swipe gesture
637      *
638      * @param swipeDirection The direction to perform the swipe in
639      * @param swipeSpeed How fast to swipe
640      * @param swipeFraction The fraction of the screen to swipe across
641      */
swipe( SwipeDirection swipeDirection, SwipeSpeed swipeSpeed, SwipeFraction swipeFraction)642     public void swipe(
643             SwipeDirection swipeDirection, SwipeSpeed swipeSpeed, SwipeFraction swipeFraction)
644             throws IOException {
645         swipe(swipeDirection, swipeSpeed.mNumSteps, swipeFraction);
646     }
647 
getPointsToSwipe( Rect bounds, SwipeDirection swipeDirection, SwipeFraction swipeFraction)648     private List<Point> getPointsToSwipe(
649             Rect bounds, SwipeDirection swipeDirection, SwipeFraction swipeFraction) {
650         int xStart;
651         int yStart;
652         int xFinish;
653         int yFinish;
654 
655         int padXStart = 5;
656         int padXFinish = 5;
657         int padYStart = 5;
658         int padYFinish = 5;
659 
660         switch (swipeFraction) {
661             case FULL:
662                 padXStart = 0;
663                 padXFinish = 0;
664                 padYStart = 0;
665                 padYFinish = 0;
666                 break;
667             case QUARTER:
668                 padXStart = bounds.right / 4;
669                 padYStart = bounds.bottom / 4;
670                 break;
671             case HALF:
672                 padXStart = bounds.centerX();
673                 padYStart = bounds.centerY();
674                 break;
675             case THREEQUARTER:
676                 padXStart = bounds.right / 4 * 3;
677                 padYStart = bounds.bottom / 4 * 3;
678                 break;
679             case DEFAULT:
680                 break; // handled above the switch.
681         }
682 
683         switch (swipeDirection) {
684                 // Scroll left = swipe from left to right.
685             case LEFT_TO_RIGHT:
686                 xStart = bounds.left + padXStart;
687                 xFinish = bounds.right - padXFinish;
688                 yStart = bounds.centerY();
689                 yFinish = bounds.centerY();
690                 break;
691                 // Scroll right = swipe from right to left.
692             case RIGHT_TO_LEFT:
693                 xStart = bounds.right - padXStart;
694                 xFinish = bounds.left + padXFinish;
695                 yStart = bounds.centerY();
696                 yFinish = bounds.centerY();
697                 break;
698                 // Scroll up = swipe from top to bottom.
699             case TOP_TO_BOTTOM:
700                 xStart = bounds.centerX();
701                 xFinish = bounds.centerX();
702                 yStart = bounds.top + padYStart;
703                 yFinish = bounds.bottom - padYFinish;
704                 break;
705                 // Scroll down = swipe to bottom to top.
706             case BOTTOM_TO_TOP:
707             default:
708                 xStart = bounds.centerX();
709                 xFinish = bounds.centerX();
710                 yStart = bounds.bottom - padYStart;
711                 yFinish = bounds.top + padYFinish;
712                 break;
713         }
714 
715         List<Point> swipePoints = new ArrayList<>();
716         // Start Point
717         swipePoints.add(new Point(xStart, yStart));
718         // Finish Point
719         swipePoints.add(new Point(xFinish, yFinish));
720 
721         return swipePoints;
722     }
723 
724     /** Returns a Rect representing the bounds of the screen */
getScreenBounds()725     public Rect getScreenBounds() {
726         return new Rect(
727             /* left= */ 0,
728             /* top= */ 0,
729             /* right= */ mDevice.getDisplayWidth(),
730             /* bottom= */ mDevice.getDisplayHeight());
731     }
732 
swipeRight(UiObject2 uiObject)733     public void swipeRight(UiObject2 uiObject) {
734         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Right");
735         uiObject.swipe(Direction.RIGHT, SWIPE_PERCENT);
736     }
737 
swipeLeft(UiObject2 uiObject)738     public void swipeLeft(UiObject2 uiObject) {
739         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Left");
740         uiObject.swipe(Direction.LEFT, SWIPE_PERCENT);
741     }
742 
swipeUp(UiObject2 uiObject)743     public void swipeUp(UiObject2 uiObject) {
744         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Up");
745         uiObject.swipe(Direction.UP, SWIPE_PERCENT);
746     }
747 
swipeDown(UiObject2 uiObject)748     public void swipeDown(UiObject2 uiObject) {
749         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Down");
750         uiObject.swipe(Direction.DOWN, SWIPE_PERCENT);
751     }
752 
setTextForUiElement(UiObject2 uiObject, String text)753     public void setTextForUiElement(UiObject2 uiObject, String text) {
754         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Set Text");
755         validateText(text, /* type= */ "Text");
756         uiObject.setText(text);
757     }
758 
getTextForUiElement(UiObject2 uiObject)759     public String getTextForUiElement(UiObject2 uiObject) {
760         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Get Text");
761         return uiObject.getText();
762     }
763 
764     /**
765      * Scroll on the device screen using forward or backward buttons.
766      *
767      * <p>Pass Forward/Down Button Selector to scroll forward. Pass Backward/Up Button Selector to
768      * scroll backward. Method throws {@link MissingUiElementException} if the given button is not
769      * available on the Device UI.
770      *
771      * @param scrollButtonSelector {@link BySelector} for the button to use for scrolling.
772      * @return Method returns true for successful scroll else returns false
773      */
scrollUsingButton(BySelector scrollButtonSelector)774     public boolean scrollUsingButton(BySelector scrollButtonSelector)
775             throws MissingUiElementException {
776         validateSelector(scrollButtonSelector, /* action= */ "Scroll Using Button");
777         UiObject2 scrollButton = findUiObject(scrollButtonSelector);
778         validateUiObjectAndThrowMissingUiElementException(
779                 scrollButton, scrollButtonSelector, /* action= */ "Scroll Using Button");
780 
781         String previousView = getViewHierarchy();
782         if (!scrollButton.isEnabled()) {
783             // Already towards the end, cannot scroll
784             return false;
785         }
786 
787         clickAndWait(scrollButton);
788 
789         String currentView = getViewHierarchy();
790 
791         // If current view is same as previous view, scroll did not work, so return false
792         return !currentView.equals(previousView);
793     }
794 
795     /**
796      * Scroll using forward and backward buttons on device screen and find the text.
797      *
798      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
799      * available on the Device UI.
800      *
801      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
802      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
803      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
804      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
805      *     not found on the Device UI.
806      */
scrollAndFindUiObject(BySelector forward, BySelector backward, String text)807     public UiObject2 scrollAndFindUiObject(BySelector forward, BySelector backward, String text)
808             throws MissingUiElementException {
809         validateText(text, /* type= */ "Text");
810         return scrollAndFindUiObject(forward, backward, By.text(text));
811     }
812 
813     /**
814      * Scroll using forward and backward buttons on device screen and find the target UI Element.
815      *
816      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
817      * available on the Device UI.
818      *
819      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
820      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
821      * @param target {@link BySelector} for UI Element to search on device UI.
822      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
823      *     target is not found on the Device UI.
824      */
scrollAndFindUiObject( BySelector forward, BySelector backward, BySelector target)825     public UiObject2 scrollAndFindUiObject(
826             BySelector forward, BySelector backward, BySelector target)
827             throws MissingUiElementException {
828         validateSelector(forward, /* action= */ "Scroll Forward");
829         validateSelector(backward, /* action= */ "Scroll Backward");
830         validateSelector(target, /* action= */ "Find UI Object");
831         // Find the object on current page
832         UiObject2 uiObject = findUiObject(target);
833         if (isValidUiObject(uiObject)) {
834             return uiObject;
835         }
836         scrollToBeginning(backward);
837         return scrollForwardAndFindUiObject(forward, target);
838     }
839 
scrollForwardAndFindUiObject(BySelector forward, BySelector target)840     private UiObject2 scrollForwardAndFindUiObject(BySelector forward, BySelector target)
841             throws MissingUiElementException {
842         UiObject2 uiObject = findUiObject(target);
843         if (isValidUiObject(uiObject)) {
844             return uiObject;
845         }
846         int scrollCount = 0;
847         boolean canScroll = true;
848         while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) {
849             canScroll = scrollUsingButton(forward);
850             scrollCount++;
851             uiObject = findUiObject(target);
852         }
853         return uiObject;
854     }
855 
scrollToBeginning(BySelector backward)856     public void scrollToBeginning(BySelector backward) throws MissingUiElementException {
857         int scrollCount = 0;
858         boolean canScroll = true;
859         while (canScroll && scrollCount < MAX_SCROLL_COUNT) {
860             canScroll = scrollUsingButton(backward);
861             scrollCount++;
862         }
863     }
864 
865     /**
866      * Swipe in a direction until a target UI Object is found
867      *
868      * @param swipeDirection Direction to swipe
869      * @param numOfSteps Ticks per swipe
870      * @param swipeFraction How far to swipe
871      * @param target The UI Object to find
872      * @return The found object, or null if there isn't one
873      */
swipeAndFindUiObject( SwipeDirection swipeDirection, int numOfSteps, SwipeFraction swipeFraction, BySelector target)874     public UiObject2 swipeAndFindUiObject(
875             SwipeDirection swipeDirection,
876             int numOfSteps,
877             SwipeFraction swipeFraction,
878             BySelector target) {
879         validateSelector(target, "Find UI Object");
880         UiObject2 uiObject = findUiObject(target);
881         if (isValidUiObject(uiObject)) {
882             return uiObject;
883         }
884 
885         String previousView = null;
886         String currentView = getViewHierarchy();
887         while (!currentView.equals(previousView)) {
888             swipe(swipeDirection, numOfSteps, swipeFraction);
889             uiObject = findUiObject(target);
890             if (isValidUiObject(uiObject)) {
891                 return uiObject;
892             }
893             previousView = currentView;
894             currentView = getViewHierarchy();
895         }
896         return null;
897     }
898 
899     /** Returns the view hierarchy as XML, as output by `adb shell uiautomator dump`. */
getViewHierarchy()900     public String getViewHierarchy() {
901         try {
902             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
903             mDevice.dumpWindowHierarchy(outputStream);
904             outputStream.close();
905             return outputStream.toString();
906         } catch (IOException ex) {
907             throw new IllegalStateException("Unable to get view hierarchy.", ex);
908         }
909     }
910 
911     /**
912      * Scroll by performing forward and backward gestures on device screen and find the text.
913      *
914      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
915      * scrollAndFindUiObject(BySelector scrollableSelector, String text, boolean isVertical)} by
916      * passing isVertical = false.
917      *
918      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
919      * available on the Device UI.
920      *
921      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
922      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
923      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
924      *     not found on the Device UI.
925      */
scrollAndFindUiObject(BySelector scrollableSelector, String text)926     public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, String text)
927             throws MissingUiElementException {
928         validateText(text, /* type= */ "Text");
929         return scrollAndFindUiObject(scrollableSelector, By.text(text));
930     }
931 
932     /**
933      * Scroll by performing forward and backward gestures on device screen and find the text.
934      *
935      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
936      * false.
937      *
938      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
939      * available on the Device UI.
940      *
941      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
942      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
943      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
944      *     not found on the Device UI.
945      */
scrollAndFindUiObject( BySelector scrollableSelector, String text, boolean isVertical)946     public UiObject2 scrollAndFindUiObject(
947             BySelector scrollableSelector, String text, boolean isVertical)
948             throws MissingUiElementException {
949         validateText(text, /* type= */ "Text");
950         return scrollAndFindUiObject(scrollableSelector, By.text(text), isVertical);
951     }
952 
953     /**
954      * Scroll by performing forward and backward gestures on device screen and find the target UI
955      * Element.
956      *
957      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
958      * scrollAndFindUiObject(BySelector scrollableSelector, BySelector target, boolean isVertical)}
959      * by passing isVertical = false.
960      *
961      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
962      * available on the Device UI.
963      *
964      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
965      * @param target {@link BySelector} for UI Element to search on device UI.
966      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
967      *     target is not found on the Device UI.
968      */
scrollAndFindUiObject(BySelector scrollableSelector, BySelector target)969     public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, BySelector target)
970             throws MissingUiElementException {
971         return scrollAndFindUiObject(scrollableSelector, target, /* isVertical= */ true);
972     }
973 
974     /**
975      * Scroll by performing forward and backward gestures on device screen and find the target UI
976      * Element.
977      *
978      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
979      * false.
980      *
981      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
982      * available on the Device UI.
983      *
984      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
985      * @param target {@link BySelector} for UI Element to search on device UI.
986      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
987      *     use isVertical = false.
988      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
989      *     target is not found on the Device UI.
990      */
scrollAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)991     public UiObject2 scrollAndFindUiObject(
992             BySelector scrollableSelector, BySelector target, boolean isVertical)
993             throws MissingUiElementException {
994         validateSelector(scrollableSelector, /* action= */ "Scroll");
995         validateSelector(target, /* action= */ "Find UI Object");
996         // Find UI element on current page
997         UiObject2 uiObject = findUiObject(target);
998         if (isValidUiObject(uiObject)) {
999             return uiObject;
1000         }
1001         scrollToBeginning(scrollableSelector, isVertical);
1002         return scrollForwardAndFindUiObject(scrollableSelector, target, isVertical);
1003     }
1004 
scrollForwardAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)1005     private UiObject2 scrollForwardAndFindUiObject(
1006             BySelector scrollableSelector, BySelector target, boolean isVertical)
1007             throws MissingUiElementException {
1008         UiObject2 uiObject = findUiObject(target);
1009         if (isValidUiObject(uiObject)) {
1010             return uiObject;
1011         }
1012         int scrollCount = 0;
1013         boolean canScroll = true;
1014         while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) {
1015             canScroll = scrollForward(scrollableSelector, isVertical);
1016             scrollCount++;
1017             uiObject = findUiObject(target);
1018         }
1019         return uiObject;
1020     }
1021 
scrollToBeginning(BySelector scrollableSelector, boolean isVertical)1022     public void scrollToBeginning(BySelector scrollableSelector, boolean isVertical)
1023             throws MissingUiElementException {
1024         int scrollCount = 0;
1025         boolean canScroll = true;
1026         while (canScroll && scrollCount < MAX_SCROLL_COUNT) {
1027             canScroll = scrollBackward(scrollableSelector, isVertical);
1028             scrollCount++;
1029         }
1030     }
1031 
getDirection(boolean isVertical, boolean scrollForward)1032     private Direction getDirection(boolean isVertical, boolean scrollForward) {
1033         // Default Scroll = Vertical and Forward
1034         // Go DOWN to scroll forward vertically
1035         Direction direction = Direction.DOWN;
1036         if (isVertical && !scrollForward) {
1037             // Scroll = Vertical and Backward
1038             // Go UP to scroll backward vertically
1039             direction = Direction.UP;
1040         }
1041         if (!isVertical && scrollForward) {
1042             // Scroll = Horizontal and Forward
1043             // Go RIGHT to scroll forward horizontally
1044             direction = Direction.RIGHT;
1045         }
1046         if (!isVertical && !scrollForward) {
1047             // Scroll = Horizontal and Backward
1048             // Go LEFT to scroll backward horizontally
1049             direction = Direction.LEFT;
1050         }
1051         return direction;
1052     }
1053 
validateAndGetScrollableObject(BySelector scrollableSelector)1054     private UiObject2 validateAndGetScrollableObject(BySelector scrollableSelector)
1055             throws MissingUiElementException {
1056         List<UiObject2> scrollableObjects = findUiObjects(scrollableSelector);
1057         for (UiObject2 scrollableObject : scrollableObjects) {
1058             validateUiObjectAndThrowMissingUiElementException(
1059                     scrollableObject, scrollableSelector, /* action= */ "Scroll");
1060             if (!scrollableObject.isScrollable()) {
1061                 scrollableObject = scrollableObject.findObject(By.scrollable(true));
1062             }
1063             if (scrollableObject != null && scrollableObject.isScrollable()) {
1064                 // if there are multiple, return the first UiObject that is scrollable
1065                 return scrollableObject;
1066             }
1067         }
1068         throw new IllegalStateException(
1069                 String.format(
1070                         "Cannot scroll; Could not find UI Object for selector %s that is scrollable"
1071                                 + " or have scrollable children.",
1072                         scrollableSelector));
1073     }
1074 
1075     /**
1076      * Scroll forward one page by performing forward gestures on device screen.
1077      *
1078      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
1079      * scrollForward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical =
1080      * false.
1081      *
1082      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
1083      * available on the Device UI.
1084      *
1085      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
1086      * @return Returns true for successful forward scroll, else false.
1087      */
scrollForward(BySelector scrollableSelector)1088     public boolean scrollForward(BySelector scrollableSelector) throws MissingUiElementException {
1089         return scrollForward(scrollableSelector, /* isVertical= */ true);
1090     }
1091 
1092     /**
1093      * Scroll forward one page by performing forward gestures on device screen.
1094      *
1095      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
1096      * false.
1097      *
1098      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
1099      * available on the Device UI.
1100      *
1101      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
1102      * @return Returns true for successful forward scroll, else false.
1103      */
scrollForward(BySelector scrollableSelector, boolean isVertical)1104     public boolean scrollForward(BySelector scrollableSelector, boolean isVertical)
1105             throws MissingUiElementException {
1106         return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ true));
1107     }
1108 
1109     /**
1110      * Scroll backward one page by performing backward gestures on device screen.
1111      *
1112      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
1113      * scrollBackward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical =
1114      * false.
1115      *
1116      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
1117      * available on the Device UI.
1118      *
1119      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
1120      * @return Returns true for successful backard scroll, else false.
1121      */
scrollBackward(BySelector scrollableSelector)1122     public boolean scrollBackward(BySelector scrollableSelector) throws MissingUiElementException {
1123         return scrollBackward(scrollableSelector, /* isVertical= */ true);
1124     }
1125 
1126     /**
1127      * Scroll backward one page by performing backward gestures on device screen.
1128      *
1129      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
1130      * false.
1131      *
1132      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
1133      * available on the Device UI.
1134      *
1135      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
1136      * @return Returns true for successful backward scroll, else false.
1137      */
scrollBackward(BySelector scrollableSelector, boolean isVertical)1138     public boolean scrollBackward(BySelector scrollableSelector, boolean isVertical)
1139             throws MissingUiElementException {
1140         return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ false));
1141     }
1142 
scroll(BySelector scrollableSelector, Direction direction)1143     private boolean scroll(BySelector scrollableSelector, Direction direction)
1144             throws MissingUiElementException {
1145 
1146         UiObject2 scrollableObject = validateAndGetScrollableObject(scrollableSelector);
1147 
1148         Rect bounds = scrollableObject.getVisibleBounds();
1149         int horizontalMargin = (int) (Math.abs(bounds.width()) / mScrollMargin);
1150         int verticalMargin = (int) (Math.abs(bounds.height()) / mScrollMargin);
1151 
1152         scrollableObject.setGestureMargins(
1153                 horizontalMargin, // left
1154                 verticalMargin, // top
1155                 horizontalMargin, // right
1156                 verticalMargin); // bottom
1157 
1158         String previousView = getViewHierarchy();
1159 
1160         scrollableObject.scroll(direction, SCROLL_PERCENT);
1161         waitNSeconds(mWaitTimeAfterScroll);
1162 
1163         String currentView = getViewHierarchy();
1164 
1165         // If current view is same as previous view, scroll did not work, so return false
1166         return !currentView.equals(previousView);
1167     }
1168 
validateText(String text, String type)1169     private void validateText(String text, String type) {
1170         if (Strings.isNullOrEmpty(text)) {
1171             throw new IllegalArgumentException(
1172                     String.format(
1173                             "Provide a valid %s, current %s value is either NULL or empty.",
1174                             type, type));
1175         }
1176     }
1177 
validateSelector(BySelector selector, String action)1178     private void validateSelector(BySelector selector, String action) {
1179         if (selector == null) {
1180             throw new IllegalArgumentException(
1181                     String.format(
1182                             "Cannot %s; Provide a valid selector to %s, currently it is NULL.",
1183                             action, action));
1184         }
1185     }
1186 
1187     /**
1188      * A simple null-check on a single uiObject2 instance
1189      *
1190      * @param uiObject - The object to be checked.
1191      * @param action - The UI action being performed when the object was generated or searched-for.
1192      */
validateUiObject(UiObject2 uiObject, String action)1193     public void validateUiObject(UiObject2 uiObject, String action) {
1194         if (uiObject == null) {
1195             throw new MissingUiElementException(
1196                     String.format("Unable to find UI Element for %s.", action));
1197         }
1198     }
1199 
1200     /**
1201      * A simple null-check on a list of UIObjects
1202      *
1203      * @param uiObjects - The list to check
1204      * @param action - A string description of the UI action being taken when this list was
1205      *     generated.
1206      */
validateUiObjects(List<UiObject2> uiObjects, String action)1207     public void validateUiObjects(List<UiObject2> uiObjects, String action) {
1208         if (uiObjects == null) {
1209             throw new MissingUiElementException(
1210                     String.format("Unable to find UI Element for %s.", action));
1211         }
1212     }
1213 
isValidUiObject(UiObject2 uiObject)1214     public boolean isValidUiObject(UiObject2 uiObject) {
1215         return uiObject != null;
1216     }
1217 
1218     private Set<String> mSupportedProperties =
1219             Set.of(
1220                     JsonConfigConstants.CLICKABLE,
1221                     JsonConfigConstants.SCROLLABLE,
1222                     JsonConfigConstants.TEXT,
1223                     JsonConfigConstants.TEXT_CONTAINS,
1224                     JsonConfigConstants.DESCRIPTION,
1225                     JsonConfigConstants.DESCRIPTION_CONTAINS,
1226                     JsonConfigConstants.CLASS,
1227                     JsonConfigConstants.DISPLAY_ID,
1228                     JsonConfigConstants.RESOURCE_ID);
1229 
1230     /**
1231      * Check a UI Object for a given property value.
1232      *
1233      * @param uiObject object to check
1234      * @param property which property to check
1235      * @param expected expected value of property
1236      */
validateUiObjectProperty(UiObject2 uiObject, String property, String expected)1237     public boolean validateUiObjectProperty(UiObject2 uiObject, String property, String expected) {
1238         if (!mSupportedProperties.contains(property)) {
1239             throw new RuntimeException(
1240                     String.format(
1241                             "VALIDATE_VALUE property name %s in Spectatio JSON Config is invalid. "
1242                                     + "Supported properties: [ RESOURCE_ID, TEXT, TEXT_CONTAINS, "
1243                                     + "DESCRIPTION, DESCRIPTION_CONTAINS, CLASS, CLICKABLE, "
1244                                     + "SCROLLABLE, DISPLAY_ID ]",
1245                             property));
1246         }
1247         switch (property) {
1248             case JsonConfigConstants.CLICKABLE:
1249                 return Boolean.toString(uiObject.isClickable()).equalsIgnoreCase(expected);
1250             case JsonConfigConstants.SCROLLABLE:
1251                 return Boolean.toString(uiObject.isScrollable()).equalsIgnoreCase(expected);
1252             case JsonConfigConstants.TEXT:
1253                 return uiObject.getText().equalsIgnoreCase(expected);
1254             case JsonConfigConstants.TEXT_CONTAINS:
1255                 return uiObject.getText().contains(expected);
1256             case JsonConfigConstants.DESCRIPTION:
1257                 return uiObject.getContentDescription().equalsIgnoreCase(expected);
1258             case JsonConfigConstants.DESCRIPTION_CONTAINS:
1259                 return uiObject.getContentDescription().contains(expected);
1260             case JsonConfigConstants.CLASS:
1261                 return uiObject.getClassName().equalsIgnoreCase(expected);
1262             case JsonConfigConstants.DISPLAY_ID:
1263                 return Integer.toString(uiObject.getDisplayId()).equals(expected);
1264             case JsonConfigConstants.RESOURCE_ID:
1265                 return uiObject.getResourceName().equals(expected);
1266             default:
1267                 return false;
1268         }
1269     }
1270 
validateUiObjectAndThrowIllegalArgumentException( UiObject2 uiObject, String action)1271     private void validateUiObjectAndThrowIllegalArgumentException(
1272             UiObject2 uiObject, String action) {
1273         if (!isValidUiObject(uiObject)) {
1274             throw new IllegalArgumentException(
1275                     String.format(
1276                             "Cannot %s; Provide a valid UI Object to %s, currently it is NULL.",
1277                             action, action));
1278         }
1279     }
1280 
validateUiObjectAndThrowMissingUiElementException( UiObject2 uiObject, BySelector selector, String action)1281     private void validateUiObjectAndThrowMissingUiElementException(
1282             UiObject2 uiObject, BySelector selector, String action)
1283             throws MissingUiElementException {
1284         if (!isValidUiObject(uiObject)) {
1285             throw new MissingUiElementException(
1286                     String.format(
1287                             "Cannot %s; Unable to find UI Object for %s selector.",
1288                             action, selector));
1289         }
1290     }
1291 }
1292