• 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.exceptions.MissingUiElementException;
25 import android.util.Log;
26 import android.view.KeyEvent;
27 
28 import androidx.test.uiautomator.By;
29 import androidx.test.uiautomator.BySelector;
30 import androidx.test.uiautomator.Direction;
31 import androidx.test.uiautomator.UiDevice;
32 import androidx.test.uiautomator.UiObject2;
33 import androidx.test.uiautomator.Until;
34 
35 import com.google.common.base.Strings;
36 
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
39 import java.util.ArrayList;
40 import java.util.List;
41 import java.util.Locale;
42 
43 public class SpectatioUiUtil {
44     private static final String LOG_TAG = SpectatioUiUtil.class.getSimpleName();
45 
46     private static SpectatioUiUtil sSpectatioUiUtil = null;
47 
48     private static final int SHORT_UI_RESPONSE_WAIT_MS = 1000;
49     private static final int LONG_UI_RESPONSE_WAIT_MS = 5000;
50     private static final int EXTRA_LONG_UI_RESPONSE_WAIT_MS = 15000;
51     private static final int LONG_PRESS_DURATION_MS = 5000;
52     private static final int MAX_SCROLL_COUNT = 100;
53     private static final int MAX_SWIPE_STEPS = 10;
54     private static final float SCROLL_PERCENT = 1.0f;
55     private static final float SWIPE_PERCENT = 1.0f;
56 
57     private int mWaitTimeAfterScroll = 5; // seconds
58     private int mScrollMargin = 4;
59 
60     private UiDevice mDevice;
61 
62     public enum SwipeDirection {
63         TOP_TO_BOTTOM,
64         BOTTOM_TO_TOP,
65         LEFT_TO_RIGHT,
66         RIGHT_TO_LEFT
67     }
68 
SpectatioUiUtil(UiDevice mDevice)69     private SpectatioUiUtil(UiDevice mDevice) {
70         this.mDevice = mDevice;
71     }
72 
getInstance(UiDevice mDevice)73     public static SpectatioUiUtil getInstance(UiDevice mDevice) {
74         if (sSpectatioUiUtil == null) {
75             sSpectatioUiUtil = new SpectatioUiUtil(mDevice);
76         }
77         return sSpectatioUiUtil;
78     }
79 
80     /**
81      * Initialize a UiDevice for the given instrumentation, then initialize Spectatio for that
82      * device. If Spectatio has already been initialized, return the previously initialized
83      * instance.
84      */
getInstance(Instrumentation instrumentation)85     public static SpectatioUiUtil getInstance(Instrumentation instrumentation) {
86         return getInstance(UiDevice.getInstance(instrumentation));
87     }
88 
89     /** Sets the scroll margin and wait time after the scroll */
addScrollValues(Integer scrollMargin, Integer waitTime)90     public void addScrollValues(Integer scrollMargin, Integer waitTime) {
91         this.mScrollMargin = scrollMargin;
92         this.mWaitTimeAfterScroll = waitTime;
93     }
94 
pressBack()95     public boolean pressBack() {
96         return mDevice.pressBack();
97     }
98 
pressHome()99     public boolean pressHome() {
100         return mDevice.pressHome();
101     }
102 
pressKeyCode(int keyCode)103     public boolean pressKeyCode(int keyCode) {
104         return mDevice.pressKeyCode(keyCode);
105     }
106 
pressPower()107     public boolean pressPower() {
108         return pressKeyCode(KeyEvent.KEYCODE_POWER);
109     }
110 
longPress(UiObject2 uiObject)111     public boolean longPress(UiObject2 uiObject) {
112         if (!isValidUiObject(uiObject)) {
113             Log.e(
114                     LOG_TAG,
115                     "Cannot Long Press UI Object; Provide a valid UI Object, currently it is"
116                             + " NULL.");
117             return false;
118         }
119         if (!uiObject.isLongClickable()) {
120             Log.e(
121                     LOG_TAG,
122                     "Cannot Long Press UI Object; Provide a valid UI Object, "
123                             + "current UI Object is not long clickable.");
124             return false;
125         }
126         uiObject.longClick();
127         wait1Second();
128         return true;
129     }
130 
longPressKey(int keyCode)131     public boolean longPressKey(int keyCode) {
132         try {
133             // Use English Locale because ADB Shell command does not depend on Device UI
134             mDevice.executeShellCommand(
135                     String.format(Locale.ENGLISH, "input keyevent --longpress %d", keyCode));
136             wait1Second();
137             return true;
138         } catch (IOException e) {
139             // Ignore
140             Log.e(
141                     LOG_TAG,
142                     String.format(
143                             "Failed to long press key code: %d, Error: %s",
144                             keyCode, e.getMessage()));
145         }
146         return false;
147     }
148 
longPressPower()149     public boolean longPressPower() {
150         return longPressKey(KeyEvent.KEYCODE_POWER);
151     }
152 
longPressScreenCenter()153     public boolean longPressScreenCenter() {
154         Rect bounds = getScreenBounds();
155         int xCenter = bounds.centerX();
156         int yCenter = bounds.centerY();
157         try {
158             // Click method in UiDevice only takes x and y co-ordintes to tap,
159             // so it can be clicked but cannot be pressed for long time
160             // Use ADB command to Swipe instead (because UiDevice swipe method don't take duration)
161             // i.e. simulate long press by swiping from
162             // center of screen to center of screen (i.e. same points) for long duration
163             // Use English Locale because ADB Shell command does not depend on Device UI
164             mDevice.executeShellCommand(
165                     String.format(
166                             Locale.ENGLISH,
167                             "input swipe %d %d %d %d %d",
168                             xCenter,
169                             yCenter,
170                             xCenter,
171                             yCenter,
172                             LONG_PRESS_DURATION_MS));
173             wait1Second();
174             return true;
175         } catch (IOException e) {
176             // Ignore
177             Log.e(
178                     LOG_TAG,
179                     String.format(
180                             "Failed to long press on screen center. Error: %s", e.getMessage()));
181         }
182         return false;
183     }
184 
wakeUp()185     public void wakeUp() {
186         try {
187             mDevice.wakeUp();
188         } catch (RemoteException ex) {
189             throw new IllegalStateException("Failed to wake up device.", ex);
190         }
191     }
192 
clickAndWait(UiObject2 uiObject)193     public void clickAndWait(UiObject2 uiObject) {
194         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Click");
195         uiObject.click();
196         wait1Second();
197     }
198 
199     /**
200      * Click at a specific location in the UI, and wait one second
201      *
202      * @param location Where to click
203      */
clickAndWait(Point location)204     public void clickAndWait(Point location) {
205         mDevice.click(location.x, location.y);
206         wait1Second();
207     }
208 
waitForIdle()209     public void waitForIdle() {
210         mDevice.waitForIdle();
211     }
212 
wait1Second()213     public void wait1Second() {
214         waitNSeconds(SHORT_UI_RESPONSE_WAIT_MS);
215     }
216 
wait5Seconds()217     public void wait5Seconds() {
218         waitNSeconds(LONG_UI_RESPONSE_WAIT_MS);
219     }
220 
221     /** Waits for 15 seconds */
wait15Seconds()222     public void wait15Seconds() {
223         waitNSeconds(EXTRA_LONG_UI_RESPONSE_WAIT_MS);
224     }
225 
waitNSeconds(int waitTime)226     public void waitNSeconds(int waitTime) {
227         SystemClock.sleep(waitTime);
228     }
229 
230     /**
231      * Executes a shell command on device, and return the standard output in string.
232      *
233      * @param command the command to run
234      * @return the standard output of the command, or empty string if failed without throwing an
235      *     IOException
236      */
executeShellCommand(String command)237     public String executeShellCommand(String command) {
238         validateText(command, /* type= */ "Command");
239         try {
240             return mDevice.executeShellCommand(command);
241         } catch (IOException e) {
242             // ignore
243             Log.e(
244                     LOG_TAG,
245                     String.format(
246                             "The shell command failed to run: %s, Error: %s",
247                             command, e.getMessage()));
248             return "";
249         }
250     }
251 
252     /** Find and return the UI Object that matches the given selector */
findUiObject(BySelector selector)253     public UiObject2 findUiObject(BySelector selector) {
254         validateSelector(selector, /* action= */ "Find UI Object");
255         UiObject2 uiObject = mDevice.wait(Until.findObject(selector), LONG_UI_RESPONSE_WAIT_MS);
256         return uiObject;
257     }
258 
259     /** Find and return the UI Objects that matches the given selector */
findUiObjects(BySelector selector)260     public List<UiObject2> findUiObjects(BySelector selector) {
261         validateSelector(selector, /* action= */ "Find UI Object");
262         List<UiObject2> uiObjects =
263                 mDevice.wait(Until.findObjects(selector), LONG_UI_RESPONSE_WAIT_MS);
264         return uiObjects;
265     }
266 
267     /**
268      * Find the UI Object that matches the given text string.
269      *
270      * @param text Text to search on device UI. It should exactly match the text visible on UI.
271      */
findUiObject(String text)272     public UiObject2 findUiObject(String text) {
273         validateText(text, /* type= */ "Text");
274         return findUiObject(By.text(text));
275     }
276 
277     /**
278      * Find the UI Object in given element.
279      *
280      * @param uiObject Find the ui object(selector) in this element.
281      * @param selector Find this ui object in the given element.
282      */
findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector)283     public UiObject2 findUiObjectInGivenElement(UiObject2 uiObject, BySelector selector) {
284         validateUiObjectAndThrowIllegalArgumentException(
285                 uiObject, /* action= */ "Find UI object in given element");
286         validateSelector(selector, /* action= */ "Find UI object in given element");
287         return uiObject.findObject(selector);
288     }
289 
290     /**
291      * Checks if given text is available on the Device UI. The text should be exactly same as seen
292      * on the screen.
293      *
294      * <p>Given text will be searched on current screen. This method will not scroll on the screen
295      * to check for given text.
296      *
297      * @param text Text to search on device UI
298      * @return Returns True if the text is found, else return False.
299      */
hasUiElement(String text)300     public boolean hasUiElement(String text) {
301         validateText(text, /* type= */ "Text");
302         return hasUiElement(By.text(text));
303     }
304 
305     /**
306      * Scroll using forward and backward buttons on device screen and check if the given text is
307      * present.
308      *
309      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
310      * available on the Device UI.
311      *
312      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
313      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
314      * @param text Text to search on device UI
315      * @return Returns True if the text is found, else return False.
316      */
scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, String text)317     public boolean scrollAndCheckIfUiElementExist(
318             BySelector forward, BySelector backward, String text) throws MissingUiElementException {
319         return scrollAndFindUiObject(forward, backward, text) != null;
320     }
321 
322     /**
323      * Scroll by performing forward and backward gestures on device screen and check if the given
324      * text is present on Device UI.
325      *
326      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
327      * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text, boolean
328      * isVertical)} by passing isVertical = false.
329      *
330      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
331      * available on the Device UI.
332      *
333      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
334      * @param text Text to search on device UI
335      * @return Returns True if the text is found, else return False.
336      */
scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text)337     public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, String text)
338             throws MissingUiElementException {
339         return scrollAndCheckIfUiElementExist(scrollableSelector, text, /* isVertical= */ true);
340     }
341 
342     /**
343      * Scroll by performing forward and backward gestures on device screen and check if the given
344      * text is present on Device UI.
345      *
346      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
347      * available on the Device UI.
348      *
349      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
350      * @param text Text to search on device UI
351      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
352      *     use isVertical = false.
353      * @return Returns True if the text is found, else return False.
354      */
scrollAndCheckIfUiElementExist( BySelector scrollableSelector, String text, boolean isVertical)355     public boolean scrollAndCheckIfUiElementExist(
356             BySelector scrollableSelector, String text, boolean isVertical)
357             throws MissingUiElementException {
358         return scrollAndFindUiObject(scrollableSelector, text, isVertical) != null;
359     }
360 
361     /**
362      * Checks if given target is available on the Device UI.
363      *
364      * <p>Given target will be searched on current screen. This method will not scroll on the screen
365      * to check for given target.
366      *
367      * @param target {@link BySelector} to search on device UI
368      * @return Returns True if the target is found, else return False.
369      */
hasUiElement(BySelector target)370     public boolean hasUiElement(BySelector target) {
371         validateSelector(target, /* action= */ "Check For UI Object");
372         return mDevice.hasObject(target);
373     }
374 
375     /**
376      * Scroll using forward and backward buttons on device screen and check if the given target is
377      * present.
378      *
379      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
380      * available on the Device UI.
381      *
382      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
383      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
384      * @param target {@link BySelector} to search on device UI
385      * @return Returns True if the target is found, else return False.
386      */
scrollAndCheckIfUiElementExist( BySelector forward, BySelector backward, BySelector target)387     public boolean scrollAndCheckIfUiElementExist(
388             BySelector forward, BySelector backward, BySelector target)
389             throws MissingUiElementException {
390         return scrollAndFindUiObject(forward, backward, target) != null;
391     }
392 
393     /**
394      * Scroll by performing forward and backward gestures on device screen and check if the target
395      * UI Element is present.
396      *
397      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
398      * scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target, boolean
399      * isVertical)} by passing isVertical = false.
400      *
401      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
402      * available on the Device UI.
403      *
404      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
405      * @param target {@link BySelector} to search on device UI
406      * @return Returns True if the target is found, else return False.
407      */
scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target)408     public boolean scrollAndCheckIfUiElementExist(BySelector scrollableSelector, BySelector target)
409             throws MissingUiElementException {
410         return scrollAndCheckIfUiElementExist(scrollableSelector, target, /* isVertical= */ true);
411     }
412 
413     /**
414      * Scroll by performing forward and backward gestures on device screen and check if the target
415      * UI Element is present.
416      *
417      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
418      * available on the Device UI.
419      *
420      * @param scrollableSelector {@link BySelector} used for scrolling on device UI
421      * @param target {@link BySelector} to search on device UI
422      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
423      *     use isVertical = false.
424      * @return Returns True if the target is found, else return False.
425      */
scrollAndCheckIfUiElementExist( BySelector scrollableSelector, BySelector target, boolean isVertical)426     public boolean scrollAndCheckIfUiElementExist(
427             BySelector scrollableSelector, BySelector target, boolean isVertical)
428             throws MissingUiElementException {
429         return scrollAndFindUiObject(scrollableSelector, target, isVertical) != null;
430     }
431 
hasPackageInForeground(String packageName)432     public boolean hasPackageInForeground(String packageName) {
433         validateText(packageName, /* type= */ "Package");
434         return mDevice.hasObject(By.pkg(packageName).depth(0));
435     }
436 
swipeUp()437     public void swipeUp() {
438         // Swipe Up From bottom of screen to the top in one step
439         swipe(SwipeDirection.BOTTOM_TO_TOP, /*numOfSteps*/ MAX_SWIPE_STEPS);
440     }
441 
swipeDown()442     public void swipeDown() {
443         // Swipe Down From top of screen to the bottom in one step
444         swipe(SwipeDirection.TOP_TO_BOTTOM, /*numOfSteps*/ MAX_SWIPE_STEPS);
445     }
446 
swipeRight()447     public void swipeRight() {
448         // Swipe Right From left of screen to the right in one step
449         swipe(SwipeDirection.LEFT_TO_RIGHT, /*numOfSteps*/ MAX_SWIPE_STEPS);
450     }
451 
swipeLeft()452     public void swipeLeft() {
453         // Swipe Left From right of screen to the left in one step
454         swipe(SwipeDirection.RIGHT_TO_LEFT, /*numOfSteps*/ MAX_SWIPE_STEPS);
455     }
456 
swipe(SwipeDirection swipeDirection, int numOfSteps)457     public void swipe(SwipeDirection swipeDirection, int numOfSteps) {
458         Rect bounds = getScreenBounds();
459 
460         List<Point> swipePoints = getPointsToSwipe(bounds, swipeDirection);
461 
462         Point startPoint = swipePoints.get(0);
463         Point finishPoint = swipePoints.get(1);
464 
465         // Swipe from start pont to finish point in given number of steps
466         mDevice.swipe(startPoint.x, startPoint.y, finishPoint.x, finishPoint.y, numOfSteps);
467     }
468 
getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection)469     private List<Point> getPointsToSwipe(Rect bounds, SwipeDirection swipeDirection) {
470         Point boundsCenter = new Point(bounds.centerX(), bounds.centerY());
471 
472         int xStart;
473         int yStart;
474         int xFinish;
475         int yFinish;
476         // Set as 5 for default
477         // TODO: Make padding value dynamic based on the screen of device under test
478         int pad = 5;
479 
480         switch (swipeDirection) {
481                 // Scroll left = swipe from left to right.
482             case LEFT_TO_RIGHT:
483                 xStart = bounds.left + pad; // Pad the edges
484                 xFinish = bounds.right - pad; // Pad the edges
485                 yStart = boundsCenter.y;
486                 yFinish = boundsCenter.y;
487                 break;
488                 // Scroll right = swipe from right to left.
489             case RIGHT_TO_LEFT:
490                 xStart = bounds.right - pad; // Pad the edges
491                 xFinish = bounds.left + pad; // Pad the edges
492                 yStart = boundsCenter.y;
493                 yFinish = boundsCenter.y;
494                 break;
495                 // Scroll up = swipe from top to bottom.
496             case TOP_TO_BOTTOM:
497                 xStart = boundsCenter.x;
498                 xFinish = boundsCenter.x;
499                 yStart = bounds.top + pad; // Pad the edges
500                 yFinish = bounds.bottom - pad; // Pad the edges
501                 break;
502                 // Scroll down = swipe to bottom to top.
503             case BOTTOM_TO_TOP:
504             default:
505                 xStart = boundsCenter.x;
506                 xFinish = boundsCenter.x;
507                 yStart = bounds.bottom - pad; // Pad the edges
508                 yFinish = bounds.top + pad; // Pad the edges
509                 break;
510         }
511 
512         List<Point> swipePoints = new ArrayList<Point>();
513         // Start Point
514         swipePoints.add(new Point(xStart, yStart));
515         // Finish Point
516         swipePoints.add(new Point(xFinish, yFinish));
517 
518         return swipePoints;
519     }
520 
getScreenBounds()521     private Rect getScreenBounds() {
522         Point dimensions = mDevice.getDisplaySizeDp();
523         return new Rect(0, 0, dimensions.x, dimensions.y);
524     }
525 
swipeRight(UiObject2 uiObject)526     public void swipeRight(UiObject2 uiObject) {
527         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Right");
528         uiObject.swipe(Direction.RIGHT, SWIPE_PERCENT);
529     }
530 
swipeLeft(UiObject2 uiObject)531     public void swipeLeft(UiObject2 uiObject) {
532         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Left");
533         uiObject.swipe(Direction.LEFT, SWIPE_PERCENT);
534     }
535 
swipeUp(UiObject2 uiObject)536     public void swipeUp(UiObject2 uiObject) {
537         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Up");
538         uiObject.swipe(Direction.UP, SWIPE_PERCENT);
539     }
540 
swipeDown(UiObject2 uiObject)541     public void swipeDown(UiObject2 uiObject) {
542         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Swipe Down");
543         uiObject.swipe(Direction.DOWN, SWIPE_PERCENT);
544     }
545 
setTextForUiElement(UiObject2 uiObject, String text)546     public void setTextForUiElement(UiObject2 uiObject, String text) {
547         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Set Text");
548         validateText(text, /* type= */ "Text");
549         uiObject.setText(text);
550     }
551 
getTextForUiElement(UiObject2 uiObject)552     public String getTextForUiElement(UiObject2 uiObject) {
553         validateUiObjectAndThrowIllegalArgumentException(uiObject, /* action= */ "Get Text");
554         return uiObject.getText();
555     }
556 
557     /**
558      * Scroll on the device screen using forward or backward buttons.
559      *
560      * <p>Pass Forward/Down Button Selector to scroll forward. Pass Backward/Up Button Selector to
561      * scroll backward. Method throws {@link MissingUiElementException} if the given button is not
562      * available on the Device UI.
563      *
564      * @param scrollButtonSelector {@link BySelector} for the button to use for scrolling.
565      * @return Method returns true for successful scroll else returns false
566      */
scrollUsingButton(BySelector scrollButtonSelector)567     public boolean scrollUsingButton(BySelector scrollButtonSelector)
568             throws MissingUiElementException {
569         validateSelector(scrollButtonSelector, /* action= */ "Scroll Using Button");
570         UiObject2 scrollButton = findUiObject(scrollButtonSelector);
571         validateUiObjectAndThrowMissingUiElementException(
572                 scrollButton, scrollButtonSelector, /* action= */ "Scroll Using Button");
573 
574         String previousView = getViewHierarchy();
575         if (!scrollButton.isEnabled()) {
576             // Already towards the end, cannot scroll
577             return false;
578         }
579 
580         clickAndWait(scrollButton);
581 
582         String currentView = getViewHierarchy();
583 
584         // If current view is same as previous view, scroll did not work, so return false
585         return !currentView.equals(previousView);
586     }
587 
588     /**
589      * Scroll using forward and backward buttons on device screen and find the text.
590      *
591      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
592      * available on the Device UI.
593      *
594      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
595      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
596      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
597      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
598      *     not found on the Device UI.
599      */
scrollAndFindUiObject(BySelector forward, BySelector backward, String text)600     public UiObject2 scrollAndFindUiObject(BySelector forward, BySelector backward, String text)
601             throws MissingUiElementException {
602         validateText(text, /* type= */ "Text");
603         return scrollAndFindUiObject(forward, backward, By.text(text));
604     }
605 
606     /**
607      * Scroll using forward and backward buttons on device screen and find the target UI Element.
608      *
609      * <p>Method throws {@link MissingUiElementException} if the given button selectors are not
610      * available on the Device UI.
611      *
612      * @param forward {@link BySelector} for the button to use for scrolling forward/down.
613      * @param backward {@link BySelector} for the button to use for scrolling backward/up.
614      * @param target {@link BySelector} for UI Element to search on device UI.
615      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
616      *     target is not found on the Device UI.
617      */
scrollAndFindUiObject( BySelector forward, BySelector backward, BySelector target)618     public UiObject2 scrollAndFindUiObject(
619             BySelector forward, BySelector backward, BySelector target)
620             throws MissingUiElementException {
621         validateSelector(forward, /* action= */ "Scroll Forward");
622         validateSelector(backward, /* action= */ "Scroll Backward");
623         validateSelector(target, /* action= */ "Find UI Object");
624         // Find the object on current page
625         UiObject2 uiObject = findUiObject(target);
626         if (isValidUiObject(uiObject)) {
627             return uiObject;
628         }
629         scrollToBeginning(backward);
630         return scrollForwardAndFindUiObject(forward, target);
631     }
632 
scrollForwardAndFindUiObject(BySelector forward, BySelector target)633     private UiObject2 scrollForwardAndFindUiObject(BySelector forward, BySelector target)
634             throws MissingUiElementException {
635         UiObject2 uiObject = findUiObject(target);
636         if (isValidUiObject(uiObject)) {
637             return uiObject;
638         }
639         int scrollCount = 0;
640         boolean canScroll = true;
641         while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) {
642             canScroll = scrollUsingButton(forward);
643             scrollCount++;
644             uiObject = findUiObject(target);
645         }
646         return uiObject;
647     }
648 
scrollToBeginning(BySelector backward)649     public void scrollToBeginning(BySelector backward) throws MissingUiElementException {
650         int scrollCount = 0;
651         boolean canScroll = true;
652         while (canScroll && scrollCount < MAX_SCROLL_COUNT) {
653             canScroll = scrollUsingButton(backward);
654             scrollCount++;
655         }
656     }
657 
getViewHierarchy()658     private String getViewHierarchy() {
659         try {
660             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
661             mDevice.dumpWindowHierarchy(outputStream);
662             outputStream.close();
663             return outputStream.toString();
664         } catch (IOException ex) {
665             throw new IllegalStateException("Unable to get view hierarchy.");
666         }
667     }
668 
669     /**
670      * Scroll by performing forward and backward gestures on device screen and find the text.
671      *
672      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
673      * scrollAndFindUiObject(BySelector scrollableSelector, String text, boolean isVertical)} by
674      * passing isVertical = false.
675      *
676      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
677      * available on the Device UI.
678      *
679      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
680      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
681      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
682      *     not found on the Device UI.
683      */
scrollAndFindUiObject(BySelector scrollableSelector, String text)684     public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, String text)
685             throws MissingUiElementException {
686         validateText(text, /* type= */ "Text");
687         return scrollAndFindUiObject(scrollableSelector, By.text(text));
688     }
689 
690     /**
691      * Scroll by performing forward and backward gestures on device screen and find the text.
692      *
693      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
694      * false.
695      *
696      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
697      * available on the Device UI.
698      *
699      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
700      * @param text Text to search on device UI. It should be exactly same as visible on device UI.
701      * @return {@link UiObject2} for given text will be returned. It returns NULL if given text is
702      *     not found on the Device UI.
703      */
scrollAndFindUiObject( BySelector scrollableSelector, String text, boolean isVertical)704     public UiObject2 scrollAndFindUiObject(
705             BySelector scrollableSelector, String text, boolean isVertical)
706             throws MissingUiElementException {
707         validateText(text, /* type= */ "Text");
708         return scrollAndFindUiObject(scrollableSelector, By.text(text), isVertical);
709     }
710 
711     /**
712      * Scroll by performing forward and backward gestures on device screen and find the target UI
713      * Element.
714      *
715      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
716      * scrollAndFindUiObject(BySelector scrollableSelector, BySelector target, boolean isVertical)}
717      * by passing isVertical = false.
718      *
719      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
720      * available on the Device UI.
721      *
722      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
723      * @param target {@link BySelector} for UI Element to search on device UI.
724      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
725      *     target is not found on the Device UI.
726      */
scrollAndFindUiObject(BySelector scrollableSelector, BySelector target)727     public UiObject2 scrollAndFindUiObject(BySelector scrollableSelector, BySelector target)
728             throws MissingUiElementException {
729         return scrollAndFindUiObject(scrollableSelector, target, /* isVertical= */ true);
730     }
731 
732     /**
733      * Scroll by performing forward and backward gestures on device screen and find the target UI
734      * Element.
735      *
736      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
737      * false.
738      *
739      * <p>Method throws {@link MissingUiElementException} if the given scrollable selector is not
740      * available on the Device UI.
741      *
742      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
743      * @param target {@link BySelector} for UI Element to search on device UI.
744      * @param isVertical For vertical scrolling, use isVertical = true and For Horizontal scrolling,
745      *     use isVertical = false.
746      * @return {@link UiObject2} for target UI Element will be returned. It returns NULL if given
747      *     target is not found on the Device UI.
748      */
scrollAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)749     public UiObject2 scrollAndFindUiObject(
750             BySelector scrollableSelector, BySelector target, boolean isVertical)
751             throws MissingUiElementException {
752         validateSelector(scrollableSelector, /* action= */ "Scroll");
753         validateSelector(target, /* action= */ "Find UI Object");
754         // Find UI element on current page
755         UiObject2 uiObject = findUiObject(target);
756         if (isValidUiObject(uiObject)) {
757             return uiObject;
758         }
759         scrollToBeginning(scrollableSelector, isVertical);
760         return scrollForwardAndFindUiObject(scrollableSelector, target, isVertical);
761     }
762 
scrollForwardAndFindUiObject( BySelector scrollableSelector, BySelector target, boolean isVertical)763     private UiObject2 scrollForwardAndFindUiObject(
764             BySelector scrollableSelector, BySelector target, boolean isVertical)
765             throws MissingUiElementException {
766         UiObject2 uiObject = findUiObject(target);
767         if (isValidUiObject(uiObject)) {
768             return uiObject;
769         }
770         int scrollCount = 0;
771         boolean canScroll = true;
772         while (!isValidUiObject(uiObject) && canScroll && scrollCount < MAX_SCROLL_COUNT) {
773             canScroll = scrollForward(scrollableSelector, isVertical);
774             scrollCount++;
775             uiObject = findUiObject(target);
776         }
777         return uiObject;
778     }
779 
scrollToBeginning(BySelector scrollableSelector, boolean isVertical)780     public void scrollToBeginning(BySelector scrollableSelector, boolean isVertical)
781             throws MissingUiElementException {
782         int scrollCount = 0;
783         boolean canScroll = true;
784         while (canScroll && scrollCount < MAX_SCROLL_COUNT) {
785             canScroll = scrollBackward(scrollableSelector, isVertical);
786             scrollCount++;
787         }
788     }
789 
getDirection(boolean isVertical, boolean scrollForward)790     private Direction getDirection(boolean isVertical, boolean scrollForward) {
791         // Default Scroll = Vertical and Forward
792         // Go DOWN to scroll forward vertically
793         Direction direction = Direction.DOWN;
794         if (isVertical && !scrollForward) {
795             // Scroll = Vertical and Backward
796             // Go UP to scroll backward vertically
797             direction = Direction.UP;
798         }
799         if (!isVertical && scrollForward) {
800             // Scroll = Horizontal and Forward
801             // Go RIGHT to scroll forward horizontally
802             direction = Direction.RIGHT;
803         }
804         if (!isVertical && !scrollForward) {
805             // Scroll = Horizontal and Backward
806             // Go LEFT to scroll backward horizontally
807             direction = Direction.LEFT;
808         }
809         return direction;
810     }
811 
validateAndGetScrollableObject(BySelector scrollableSelector)812     private UiObject2 validateAndGetScrollableObject(BySelector scrollableSelector)
813             throws MissingUiElementException {
814         UiObject2 scrollableObject = findUiObject(scrollableSelector);
815         validateUiObjectAndThrowMissingUiElementException(
816                 scrollableObject, scrollableSelector, /* action= */ "Scroll");
817         if (!scrollableObject.isScrollable()) {
818             scrollableObject = scrollableObject.findObject(By.scrollable(true));
819         }
820         if ((scrollableObject == null) || !scrollableObject.isScrollable()) {
821             throw new IllegalStateException(
822                     String.format(
823                             "Cannot scroll; UI Object for selector %s is not scrollable and has no"
824                                     + " scrollable children.",
825                             scrollableSelector));
826         }
827         return scrollableObject;
828     }
829 
830     /**
831      * Scroll forward one page by performing forward gestures on device screen.
832      *
833      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
834      * scrollForward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical =
835      * false.
836      *
837      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
838      * available on the Device UI.
839      *
840      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
841      * @return Returns true for successful forward scroll, else false.
842      */
scrollForward(BySelector scrollableSelector)843     public boolean scrollForward(BySelector scrollableSelector) throws MissingUiElementException {
844         return scrollForward(scrollableSelector, /* isVertical= */ true);
845     }
846 
847     /**
848      * Scroll forward one page by performing forward gestures on device screen.
849      *
850      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
851      * false.
852      *
853      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
854      * available on the Device UI.
855      *
856      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
857      * @return Returns true for successful forward scroll, else false.
858      */
scrollForward(BySelector scrollableSelector, boolean isVertical)859     public boolean scrollForward(BySelector scrollableSelector, boolean isVertical)
860             throws MissingUiElementException {
861         return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ true));
862     }
863 
864     /**
865      * Scroll backward one page by performing backward gestures on device screen.
866      *
867      * <p>Scrolling will be performed vertically by default. For horizontal scrolling use {@code
868      * scrollBackward(BySelector scrollableSelector, boolean isVertical)} by passing isVertical =
869      * false.
870      *
871      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
872      * available on the Device UI.
873      *
874      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
875      * @return Returns true for successful backard scroll, else false.
876      */
scrollBackward(BySelector scrollableSelector)877     public boolean scrollBackward(BySelector scrollableSelector) throws MissingUiElementException {
878         return scrollBackward(scrollableSelector, /* isVertical= */ true);
879     }
880 
881     /**
882      * Scroll backward one page by performing backward gestures on device screen.
883      *
884      * <p>For vertical scrolling, use isVertical = true For Horizontal scrolling, use isVertical =
885      * false.
886      *
887      * <p>Method throws {@link MissingUiElementException} if given scrollable selector is not
888      * available on the Device UI.
889      *
890      * @param scrollableSelector {@link BySelector} for the scrollable UI Element on device UI.
891      * @return Returns true for successful backward scroll, else false.
892      */
scrollBackward(BySelector scrollableSelector, boolean isVertical)893     public boolean scrollBackward(BySelector scrollableSelector, boolean isVertical)
894             throws MissingUiElementException {
895         return scroll(scrollableSelector, getDirection(isVertical, /* scrollForward= */ false));
896     }
897 
scroll(BySelector scrollableSelector, Direction direction)898     private boolean scroll(BySelector scrollableSelector, Direction direction)
899             throws MissingUiElementException {
900 
901         UiObject2 scrollableObject = validateAndGetScrollableObject(scrollableSelector);
902 
903         Rect bounds = scrollableObject.getVisibleBounds();
904         int horizontalMargin = (int) (Math.abs(bounds.width()) / mScrollMargin);
905         int verticalMargin = (int) (Math.abs(bounds.height()) / mScrollMargin);
906 
907         scrollableObject.setGestureMargins(
908                 horizontalMargin, // left
909                 verticalMargin, // top
910                 horizontalMargin, // right
911                 verticalMargin); // bottom
912 
913         String previousView = getViewHierarchy();
914 
915         scrollableObject.scroll(direction, SCROLL_PERCENT);
916         waitNSeconds(mWaitTimeAfterScroll);
917 
918         String currentView = getViewHierarchy();
919 
920         // If current view is same as previous view, scroll did not work, so return false
921         return !currentView.equals(previousView);
922     }
923 
validateText(String text, String type)924     private void validateText(String text, String type) {
925         if (Strings.isNullOrEmpty(text)) {
926             throw new IllegalArgumentException(
927                     String.format(
928                             "Provide a valid %s, current %s value is either NULL or empty.",
929                             type, type));
930         }
931     }
932 
validateSelector(BySelector selector, String action)933     private void validateSelector(BySelector selector, String action) {
934         if (selector == null) {
935             throw new IllegalArgumentException(
936                     String.format(
937                             "Cannot %s; Provide a valid selector to %s, currently it is NULL.",
938                             action, action));
939         }
940     }
941 
942     /**
943      * A simple null-check on a single uiObject2 instance
944      *
945      * @param uiObject - The object to be checked.
946      * @param action - The UI action being performed when the object was generated or searched-for.
947      */
validateUiObject(UiObject2 uiObject, String action)948     public void validateUiObject(UiObject2 uiObject, String action) {
949         if (uiObject == null) {
950             throw new MissingUiElementException(
951                     String.format("Unable to find UI Element for %s.", action));
952         }
953     }
954 
955     /**
956      * A simple null-check on a list of UIObjects
957      *
958      * @param uiObjects - The list to check
959      * @param action - A string description of the UI action being taken when this list was
960      *     generated.
961      */
validateUiObjects(List<UiObject2> uiObjects, String action)962     public void validateUiObjects(List<UiObject2> uiObjects, String action) {
963         if (uiObjects == null) {
964             throw new MissingUiElementException(
965                     String.format("Unable to find UI Element for %s.", action));
966         }
967     }
968 
isValidUiObject(UiObject2 uiObject)969     public boolean isValidUiObject(UiObject2 uiObject) {
970         return uiObject != null;
971     }
972 
validateUiObjectAndThrowIllegalArgumentException( UiObject2 uiObject, String action)973     private void validateUiObjectAndThrowIllegalArgumentException(
974             UiObject2 uiObject, String action) {
975         if (!isValidUiObject(uiObject)) {
976             throw new IllegalArgumentException(
977                     String.format(
978                             "Cannot %s; Provide a valid UI Object to %s, currently it is NULL.",
979                             action, action));
980         }
981     }
982 
validateUiObjectAndThrowMissingUiElementException( UiObject2 uiObject, BySelector selector, String action)983     private void validateUiObjectAndThrowMissingUiElementException(
984             UiObject2 uiObject, BySelector selector, String action)
985             throws MissingUiElementException {
986         if (!isValidUiObject(uiObject)) {
987             throw new MissingUiElementException(
988                     String.format(
989                             "Cannot %s; Unable to find UI Object for %s selector.",
990                             action, selector));
991         }
992     }
993 }
994