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