1 /*
2  * Copyright (C) 2012 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 androidx.test.uiautomator;
18 
19 import android.graphics.Point;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.os.SystemClock;
23 import android.util.Log;
24 import android.view.MotionEvent.PointerCoords;
25 import android.view.accessibility.AccessibilityNodeInfo;
26 
27 import org.jspecify.annotations.NonNull;
28 import org.jspecify.annotations.Nullable;
29 
30 /**
31  * A UiObject is a representation of a view. It is not in any way directly bound to a
32  * view as an object reference. A UiObject contains information to help it
33  * locate a matching view at runtime based on the {@link UiSelector} properties specified in
34  * its constructor. Once you create an instance of a UiObject, it can
35  * be reused for different views that match the selector criteria.
36  */
37 public class UiObject {
38     private static final String TAG = UiObject.class.getSimpleName();
39 
40     /** @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)} */
41     @Deprecated
42     protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000;
43     protected static final long WAIT_FOR_SELECTOR_POLL = 1000;
44     // set a default timeout to 5.5s, since ANR threshold is 5s
45     protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500;
46     protected static final int SWIPE_MARGIN_LIMIT = 5;
47     /** @deprecated use {@link Configurator#setScrollAcknowledgmentTimeout(long)} */
48     @Deprecated
49     protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000;
50     protected static final int FINGER_TOUCH_HALF_WIDTH = 20;
51 
52     private final UiSelector mUiSelector;
53     private final UiDevice mDevice;
54 
55     private final Configurator mConfig = Configurator.getInstance();
56 
57     /**
58      * Constructs a UiObject to represent a view that matches the specified
59      * selector criteria.
60      *
61      * @deprecated Use {@link UiDevice#findObject(UiSelector)} instead. This version hides
62      * UiObject's dependency on UiDevice and is prone to misuse.
63      * @param selector
64      */
65     @Deprecated
UiObject(UiSelector selector)66     public UiObject(UiSelector selector) {
67         mUiSelector = selector;
68         mDevice = UiDevice.getInstance();
69     }
70 
71     /**
72      * Package-private constructor. Used by {@link UiDevice#findObject(UiSelector)} to construct a
73      * UiObject.
74      */
UiObject(UiDevice device, UiSelector selector)75     UiObject(UiDevice device, UiSelector selector) {
76         mDevice = device;
77         mUiSelector = selector;
78     }
79 
80     /**
81      * Debugging helper. A test can dump the properties of a selector as a string
82      * to its logs if needed. <code>getSelector().toString();</code>
83      *
84      * @return {@link UiSelector}
85      */
getSelector()86     public final @NonNull UiSelector getSelector() {
87         if (mUiSelector == null) {
88             throw new IllegalStateException("UiSelector not set");
89         }
90         return mUiSelector;
91     }
92 
getDevice()93     UiDevice getDevice() {
94         return mDevice;
95     }
96 
97     /**
98      * Retrieves the {@link QueryController} to translate a {@link UiSelector} selector
99      * into an {@link AccessibilityNodeInfo}.
100      *
101      * @return {@link QueryController}
102      */
getQueryController()103     QueryController getQueryController() {
104         return getDevice().getQueryController();
105     }
106 
107     /**
108      * Retrieves the {@link InteractionController} to perform finger actions such as tapping,
109      * swiping, or entering text.
110      *
111      * @return {@link InteractionController}
112      */
getInteractionController()113     InteractionController getInteractionController() {
114         return getDevice().getInteractionController();
115     }
116 
117     /**
118      * Creates a new UiObject for a child view that is under the present UiObject.
119      *
120      * @param selector for child view to match
121      * @return a new UiObject representing the child view
122      */
getChild(@onNull UiSelector selector)123     public @NonNull UiObject getChild(@NonNull UiSelector selector)
124             throws UiObjectNotFoundException {
125         return new UiObject(getSelector().childSelector(selector));
126     }
127 
128     /**
129      * Creates a new UiObject for a sibling view or a child of the sibling view,
130      * relative to the present UiObject.
131      *
132      * @param selector for a sibling view or children of the sibling view
133      * @return a new UiObject representing the matched view
134      * @throws UiObjectNotFoundException
135      */
getFromParent(@onNull UiSelector selector)136     public @NonNull UiObject getFromParent(@NonNull UiSelector selector)
137             throws UiObjectNotFoundException {
138         return new UiObject(getSelector().fromParent(selector));
139     }
140 
141     /**
142      * Counts the child views immediately under the present UiObject.
143      *
144      * @return the count of child views.
145      * @throws UiObjectNotFoundException
146      */
getChildCount()147     public int getChildCount() throws UiObjectNotFoundException {
148         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
149         if(node == null) {
150             throw new UiObjectNotFoundException(mUiSelector.toString());
151         }
152         return node.getChildCount();
153     }
154 
155     /**
156      * Finds a matching UI element in the accessibility hierarchy, by
157      * using the selector for this UiObject.
158      *
159      * @param timeout in milliseconds
160      * @return AccessibilityNodeInfo if found else null
161      */
findAccessibilityNodeInfo(long timeout)162     protected @Nullable AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) {
163         AccessibilityNodeInfo node = null;
164         long startMills = SystemClock.uptimeMillis();
165         long currentMills = 0;
166         while (currentMills <= timeout) {
167             node = getQueryController().findAccessibilityNodeInfo(mUiSelector);
168             if (node != null) {
169                 break;
170             } else {
171                 // does nothing if we're reentering another runWatchers()
172                 getDevice().runWatchers();
173             }
174             currentMills = SystemClock.uptimeMillis() - startMills;
175             if(timeout > 0) {
176                 SystemClock.sleep(WAIT_FOR_SELECTOR_POLL);
177             }
178         }
179         return node;
180     }
181 
182     /**
183      * Drags this object to a destination UiObject.
184      * The number of steps specified in your input parameter can influence the
185      * drag speed, and varying speeds may impact the results. Consider
186      * evaluating different speeds when using this method in your tests.
187      *
188      * @param destObj the destination UiObject.
189      * @param steps usually 40 steps. You can increase or decrease the steps to change the speed.
190      * @return true if successful
191      * @throws UiObjectNotFoundException
192      */
dragTo(@onNull UiObject destObj, int steps)193     public boolean dragTo(@NonNull UiObject destObj, int steps) throws UiObjectNotFoundException {
194         Rect srcRect = getVisibleBounds();
195         Rect dstRect = destObj.getVisibleBounds();
196         Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.",
197                 srcRect.centerX(), srcRect.centerY(), dstRect.centerX(), dstRect.centerY(), steps));
198         return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(),
199                 dstRect.centerX(), dstRect.centerY(), steps, true);
200     }
201 
202     /**
203      * Drags this object to arbitrary coordinates.
204      * The number of steps specified in your input parameter can influence the
205      * drag speed, and varying speeds may impact the results. Consider
206      * evaluating different speeds when using this method in your tests.
207      *
208      * @param destX the X-axis coordinate.
209      * @param destY the Y-axis coordinate.
210      * @param steps usually 40 steps. You can increase or decrease the steps to change the speed.
211      * @return true if successful
212      * @throws UiObjectNotFoundException
213      */
dragTo(int destX, int destY, int steps)214     public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException {
215         Rect srcRect = getVisibleBounds();
216         Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.",
217                 srcRect.centerX(), srcRect.centerY(), destX, destY, steps));
218         return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY,
219                 steps, true);
220     }
221 
222     /**
223      * Performs the swipe up action on the UiObject.
224      * See also:
225      * <ul>
226      * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
227      * <li>{@link UiScrollable#scrollToEnd(int)}</li>
228      * <li>{@link UiScrollable#scrollBackward()}</li>
229      * <li>{@link UiScrollable#scrollForward()}</li>
230      * </ul>
231      *
232      * @param steps indicates the number of injected move steps into the system. Steps are
233      * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
234      * @return true of successful
235      * @throws UiObjectNotFoundException
236      */
swipeUp(int steps)237     public boolean swipeUp(int steps) throws UiObjectNotFoundException {
238         Rect rect = getVisibleBounds();
239         if (rect.height() <= SWIPE_MARGIN_LIMIT * 2) {
240             Log.w(TAG, String.format("Cannot swipe. Object height too small (%d < %d).",
241                     rect.height(), SWIPE_MARGIN_LIMIT * 2));
242             return false;
243         }
244         Log.d(TAG, String.format("Swiping up from (%d, %d) to (%d, %d) in %d steps.",
245                 rect.centerX(), rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(),
246                 rect.top + SWIPE_MARGIN_LIMIT, steps));
247         return getInteractionController().swipe(rect.centerX(),
248                 rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT,
249                 steps);
250     }
251 
252     /**
253      * Performs the swipe down action on the UiObject.
254      * The swipe gesture can be performed over any surface. The targeted
255      * UI element does not need to be scrollable.
256      * See also:
257      * <ul>
258      * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
259      * <li>{@link UiScrollable#scrollToEnd(int)}</li>
260      * <li>{@link UiScrollable#scrollBackward()}</li>
261      * <li>{@link UiScrollable#scrollForward()}</li>
262      * </ul>
263      *
264      * @param steps indicates the number of injected move steps into the system. Steps are
265      * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
266      * @return true if successful
267      * @throws UiObjectNotFoundException
268      */
swipeDown(int steps)269     public boolean swipeDown(int steps) throws UiObjectNotFoundException {
270         Rect rect = getVisibleBounds();
271         if (rect.height() <= SWIPE_MARGIN_LIMIT * 2) {
272             Log.w(TAG, String.format("Cannot swipe. Object height too small (%d < %d).",
273                     rect.height(), SWIPE_MARGIN_LIMIT * 2));
274             return false;
275         }
276         Log.d(TAG, String.format("Swiping down from (%d, %d) to (%d, %d) in %d steps.",
277                 rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(),
278                 rect.bottom - SWIPE_MARGIN_LIMIT, steps));
279         return getInteractionController().swipe(rect.centerX(),
280                 rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(),
281                 rect.bottom - SWIPE_MARGIN_LIMIT, steps);
282     }
283 
284     /**
285      * Performs the swipe left action on the UiObject.
286      * The swipe gesture can be performed over any surface. The targeted
287      * UI element does not need to be scrollable.
288      * See also:
289      * <ul>
290      * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
291      * <li>{@link UiScrollable#scrollToEnd(int)}</li>
292      * <li>{@link UiScrollable#scrollBackward()}</li>
293      * <li>{@link UiScrollable#scrollForward()}</li>
294      * </ul>
295      *
296      * @param steps indicates the number of injected move steps into the system. Steps are
297      * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
298      * @return true if successful
299      * @throws UiObjectNotFoundException
300      */
swipeLeft(int steps)301     public boolean swipeLeft(int steps) throws UiObjectNotFoundException {
302         Rect rect = getVisibleBounds();
303         if (rect.width() <= SWIPE_MARGIN_LIMIT * 2) {
304             Log.w(TAG, String.format("Cannot swipe. Object width too small (%d < %d).",
305                     rect.width(), SWIPE_MARGIN_LIMIT * 2));
306             return false;
307         }
308         Log.d(TAG, String.format("Swiping left from (%d, %d) to (%d, %d) in %d steps.",
309                 rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT,
310                 rect.centerY(), steps));
311         return getInteractionController().swipe(rect.right - SWIPE_MARGIN_LIMIT,
312                 rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
313     }
314 
315     /**
316      * Performs the swipe right action on the UiObject.
317      * The swipe gesture can be performed over any surface. The targeted
318      * UI element does not need to be scrollable.
319      * See also:
320      * <ul>
321      * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
322      * <li>{@link UiScrollable#scrollToEnd(int)}</li>
323      * <li>{@link UiScrollable#scrollBackward()}</li>
324      * <li>{@link UiScrollable#scrollForward()}</li>
325      * </ul>
326      *
327      * @param steps indicates the number of injected move steps into the system. Steps are
328      * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
329      * @return true if successful
330      * @throws UiObjectNotFoundException
331      */
swipeRight(int steps)332     public boolean swipeRight(int steps) throws UiObjectNotFoundException {
333         Rect rect = getVisibleBounds();
334         if (rect.width() <= SWIPE_MARGIN_LIMIT * 2) {
335             Log.w(TAG, String.format("Cannot swipe. Object width too small (%d < %d).",
336                     rect.width(), SWIPE_MARGIN_LIMIT * 2));
337             return false;
338         }
339         Log.d(TAG, String.format("Swiping right from (%d, %d) to (%d, %d) in %d steps.",
340                 rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT,
341                 rect.centerY(), steps));
342         return getInteractionController().swipe(rect.left + SWIPE_MARGIN_LIMIT,
343                 rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
344     }
345 
346     /**
347      * Finds the visible bounds of a partially visible UI element
348      *
349      * @param node
350      * @return null if node is null, else a Rect containing visible bounds
351      */
getVisibleBounds(AccessibilityNodeInfo node)352     Rect getVisibleBounds(AccessibilityNodeInfo node) {
353         if (node == null) {
354             return null;
355         }
356 
357         // targeted node's bounds
358         int w = getDevice().getDisplayWidth();
359         int h = getDevice().getDisplayHeight();
360 
361         return AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h, true);
362     }
363 
364     /**
365      * Performs a click at the center of the visible bounds of the UI element represented
366      * by this UiObject.
367      *
368      * @return true id successful else false
369      * @throws UiObjectNotFoundException
370      */
click()371     public boolean click() throws UiObjectNotFoundException {
372         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
373         if(node == null) {
374             throw new UiObjectNotFoundException(mUiSelector.toString());
375         }
376         Rect rect = getVisibleBounds(node);
377         Log.d(TAG, String.format("Clicking on (%d, %d).", rect.centerX(), rect.centerY()));
378         return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(),
379                 mConfig.getActionAcknowledgmentTimeout());
380     }
381 
382     /**
383      * Waits for window transitions that would typically take longer than the
384      * usual default timeouts.
385      * See {@link #clickAndWaitForNewWindow(long)}
386      *
387      * @return true if the event was triggered, else false
388      * @throws UiObjectNotFoundException
389      */
clickAndWaitForNewWindow()390     public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException {
391         return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT);
392     }
393 
394     /**
395      * Performs a click at the center of the visible bounds of the UI element represented
396      * by this UiObject and waits for window transitions.
397      *
398      * This method differ from {@link UiObject#click()} only in that this method waits for a
399      * a new window transition as a result of the click. Some examples of a window transition:
400      * <ul>
401      * <li>launching a new activity</li>
402      * <li>bringing up a pop-up menu</li>
403      * <li>bringing up a dialog</li>
404      * </ul>
405      *
406      * @param timeout timeout before giving up on waiting for a new window
407      * @return true if the event was triggered, else false
408      * @throws UiObjectNotFoundException
409      */
clickAndWaitForNewWindow(long timeout)410     public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException {
411         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
412         if(node == null) {
413             throw new UiObjectNotFoundException(mUiSelector.toString());
414         }
415         Rect rect = getVisibleBounds(node);
416         Log.d(TAG,
417                 String.format("Clicking on (%d, %d) and waiting %dms for new window.",
418                         rect.centerX(), rect.centerY(), timeout));
419         return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(),
420                 timeout);
421     }
422 
423     /**
424      * Clicks the top and left corner of the UI element
425      *
426      * @return true on success
427      * @throws UiObjectNotFoundException
428      */
clickTopLeft()429     public boolean clickTopLeft() throws UiObjectNotFoundException {
430         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
431         if(node == null) {
432             throw new UiObjectNotFoundException(mUiSelector.toString());
433         }
434         Rect rect = getVisibleBounds(node);
435         Log.d(TAG, String.format("Clicking on (%d, %d).", rect.left + 5, rect.top + 5));
436         return getInteractionController().clickAndSync(rect.left + 5, rect.top + 5,
437                 mConfig.getActionAcknowledgmentTimeout());
438     }
439 
440     /**
441      * Long clicks bottom and right corner of the UI element
442      *
443      * @return true if operation was successful
444      * @throws UiObjectNotFoundException
445      */
longClickBottomRight()446     public boolean longClickBottomRight() throws UiObjectNotFoundException  {
447         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
448         if(node == null) {
449             throw new UiObjectNotFoundException(mUiSelector.toString());
450         }
451         Rect rect = getVisibleBounds(node);
452         Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.right - 5, rect.bottom - 5));
453         return getInteractionController().longTapAndSync(rect.right - 5, rect.bottom - 5,
454                 mConfig.getActionAcknowledgmentTimeout());
455     }
456 
457     /**
458      * Clicks the bottom and right corner of the UI element
459      *
460      * @return true on success
461      * @throws UiObjectNotFoundException
462      */
clickBottomRight()463     public boolean clickBottomRight() throws UiObjectNotFoundException {
464         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
465         if(node == null) {
466             throw new UiObjectNotFoundException(mUiSelector.toString());
467         }
468         Rect rect = getVisibleBounds(node);
469         Log.d(TAG, String.format("Clicking on (%d, %d).", rect.right - 5, rect.bottom - 5));
470         return getInteractionController().clickAndSync(rect.right - 5, rect.bottom - 5,
471                 mConfig.getActionAcknowledgmentTimeout());
472     }
473 
474     /**
475      * Long clicks the center of the visible bounds of the UI element
476      *
477      * @return true if operation was successful
478      * @throws UiObjectNotFoundException
479      */
longClick()480     public boolean longClick() throws UiObjectNotFoundException  {
481         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
482         if(node == null) {
483             throw new UiObjectNotFoundException(mUiSelector.toString());
484         }
485         Rect rect = getVisibleBounds(node);
486         Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.centerX(), rect.centerY()));
487         return getInteractionController().longTapAndSync(rect.centerX(), rect.centerY(),
488                 mConfig.getActionAcknowledgmentTimeout());
489     }
490 
491     /**
492      * Long clicks on the top and left corner of the UI element
493      *
494      * @return true if operation was successful
495      * @throws UiObjectNotFoundException
496      */
longClickTopLeft()497     public boolean longClickTopLeft() throws UiObjectNotFoundException {
498         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
499         if(node == null) {
500             throw new UiObjectNotFoundException(mUiSelector.toString());
501         }
502         Rect rect = getVisibleBounds(node);
503         Log.d(TAG, String.format("Long-clicking on (%d, %d).", rect.left + 5, rect.top + 5));
504         return getInteractionController().longTapAndSync(rect.left + 5, rect.top + 5,
505                 mConfig.getActionAcknowledgmentTimeout());
506     }
507 
508     /**
509      * Reads the <code>text</code> property of the UI element
510      *
511      * @return text value of the current node represented by this UiObject
512      * @throws UiObjectNotFoundException if no match could be found
513      */
514     @SuppressWarnings("GetterSetterNullability")
getText()515     public @NonNull String getText() throws UiObjectNotFoundException {
516         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
517         if(node == null) {
518             throw new UiObjectNotFoundException(mUiSelector.toString());
519         }
520         return safeStringReturn(node.getText());
521     }
522 
523     /**
524      * Retrieves the <code>className</code> property of the UI element.
525      *
526      * @return class name of the current node represented by this UiObject
527      * @throws UiObjectNotFoundException if no match was found
528      */
getClassName()529     public @NonNull String getClassName() throws UiObjectNotFoundException {
530         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
531         if(node == null) {
532             throw new UiObjectNotFoundException(mUiSelector.toString());
533         }
534         return safeStringReturn(node.getClassName());
535     }
536 
537     /**
538      * Reads the <code>content_desc</code> property of the UI element
539      *
540      * @return value of node attribute "content_desc"
541      * @throws UiObjectNotFoundException
542      */
getContentDescription()543     public @NonNull String getContentDescription() throws UiObjectNotFoundException {
544         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
545         if(node == null) {
546             throw new UiObjectNotFoundException(mUiSelector.toString());
547         }
548         return safeStringReturn(node.getContentDescription());
549     }
550 
551     /**
552      * Sets the text in an editable field, after clearing the field's content.
553      *
554      * <p>
555      * The {@link UiSelector} selector of this object must reference a UI element that is editable.
556      *
557      * <p>
558      * When you call this method, the method sets focus on the editable field, clears its existing
559      * content, then injects your specified text into the field.
560      *
561      * <p>
562      * If you want to capture the original contents of the field, call {@link #getText()} first.
563      * You can then modify the text and use this method to update the field.
564      *
565      * <p><strong>Improvements: </strong>
566      * Post API Level 19 (KitKat release), the underlying implementation is updated to a dedicated
567      * set text accessibility action, and it also now supports Unicode.
568      *
569      * @param text string to set
570      * @return true if operation is successful
571      * @throws UiObjectNotFoundException
572      */
setText(@ullable String text)573     public boolean setText(@Nullable String text) throws UiObjectNotFoundException {
574         // per framework convention, setText with null means clearing it
575         if (text == null) {
576             text = "";
577         }
578         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
579         if (node == null) {
580             throw new UiObjectNotFoundException(getSelector().toString());
581         }
582         Log.d(TAG, String.format("Setting text to '%s'.", text));
583         Bundle args = new Bundle();
584         args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
585         return node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
586     }
587 
588     /**
589      * Clears the existing text contents in an editable field.
590      *
591      * The {@link UiSelector} of this object must reference a UI element that is editable.
592      *
593      * When you call this method, the method sets focus on the editable field, selects all of its
594      * existing content, and clears it by sending a DELETE key press
595      *
596      * @throws UiObjectNotFoundException
597      */
clearTextField()598     public void clearTextField() throws UiObjectNotFoundException {
599         // long click left + center
600         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
601         if(node == null) {
602             throw new UiObjectNotFoundException(mUiSelector.toString());
603         }
604         CharSequence text = node.getText();
605         // do nothing if already empty
606         if (text != null && text.length() > 0) {
607             setText("");
608         }
609     }
610 
611     /**
612      * Check if the UI element's <code>checked</code> property is currently true
613      *
614      * @return true if it is else false
615      */
isChecked()616     public boolean isChecked() throws UiObjectNotFoundException {
617         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
618         if(node == null) {
619             throw new UiObjectNotFoundException(mUiSelector.toString());
620         }
621         return node.isChecked();
622     }
623 
624     /**
625      * Checks if the UI element's <code>selected</code> property is currently true.
626      *
627      * @return true if it is else false
628      * @throws UiObjectNotFoundException
629      */
isSelected()630     public boolean isSelected() throws UiObjectNotFoundException {
631         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
632         if(node == null) {
633             throw new UiObjectNotFoundException(mUiSelector.toString());
634         }
635         return node.isSelected();
636     }
637 
638     /**
639      * Checks if the UI element's <code>checkable</code> property is currently true.
640      *
641      * @return true if it is else false
642      * @throws UiObjectNotFoundException
643      */
isCheckable()644     public boolean isCheckable() throws UiObjectNotFoundException {
645         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
646         if(node == null) {
647             throw new UiObjectNotFoundException(mUiSelector.toString());
648         }
649         return node.isCheckable();
650     }
651 
652     /**
653      * Checks if the UI element's <code>enabled</code> property is currently true.
654      *
655      * @return true if it is else false
656      * @throws UiObjectNotFoundException
657      */
isEnabled()658     public boolean isEnabled() throws UiObjectNotFoundException {
659         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
660         if(node == null) {
661             throw new UiObjectNotFoundException(mUiSelector.toString());
662         }
663         return node.isEnabled();
664     }
665 
666     /**
667      * Checks if the UI element's <code>clickable</code> property is currently true.
668      *
669      * @return true if it is else false
670      * @throws UiObjectNotFoundException
671      */
isClickable()672     public boolean isClickable() throws UiObjectNotFoundException {
673         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
674         if(node == null) {
675             throw new UiObjectNotFoundException(mUiSelector.toString());
676         }
677         return node.isClickable();
678     }
679 
680     /**
681      * Check if the UI element's <code>focused</code> property is currently true
682      *
683      * @return true if it is else false
684      * @throws UiObjectNotFoundException
685      */
isFocused()686     public boolean isFocused() throws UiObjectNotFoundException {
687         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
688         if(node == null) {
689             throw new UiObjectNotFoundException(mUiSelector.toString());
690         }
691         return node.isFocused();
692     }
693 
694     /**
695      * Check if the UI element's <code>focusable</code> property is currently true.
696      *
697      * @return true if it is else false
698      * @throws UiObjectNotFoundException
699      */
isFocusable()700     public boolean isFocusable() throws UiObjectNotFoundException {
701         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
702         if(node == null) {
703             throw new UiObjectNotFoundException(mUiSelector.toString());
704         }
705         return node.isFocusable();
706     }
707 
708     /**
709      * Check if the view's <code>scrollable</code> property is currently true
710      *
711      * @return true if it is else false
712      * @throws UiObjectNotFoundException
713      */
isScrollable()714     public boolean isScrollable() throws UiObjectNotFoundException {
715         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
716         if(node == null) {
717             throw new UiObjectNotFoundException(mUiSelector.toString());
718         }
719         return node.isScrollable();
720     }
721 
722     /**
723      * Check if the view's <code>long-clickable</code> property is currently true
724      *
725      * @return true if it is else false
726      * @throws UiObjectNotFoundException
727      */
isLongClickable()728     public boolean isLongClickable() throws UiObjectNotFoundException {
729         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
730         if(node == null) {
731             throw new UiObjectNotFoundException(mUiSelector.toString());
732         }
733         return node.isLongClickable();
734     }
735 
736     /**
737      * Reads the view's <code>package</code> property
738      *
739      * @return true if it is else false
740      * @throws UiObjectNotFoundException
741      */
getPackageName()742     public @NonNull String getPackageName() throws UiObjectNotFoundException {
743         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
744         if(node == null) {
745             throw new UiObjectNotFoundException(mUiSelector.toString());
746         }
747         return safeStringReturn(node.getPackageName());
748     }
749 
750     /**
751      * Returns the visible bounds of the view.
752      *
753      * If a portion of the view is visible, only the bounds of the visible portion are
754      * reported.
755      *
756      * @return Rect
757      * @throws UiObjectNotFoundException
758      * @see #getBounds()
759      */
getVisibleBounds()760     public @NonNull Rect getVisibleBounds() throws UiObjectNotFoundException {
761         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
762         if(node == null) {
763             throw new UiObjectNotFoundException(mUiSelector.toString());
764         }
765         return getVisibleBounds(node);
766     }
767 
768     /**
769      * Returns the view's <code>bounds</code> property. See {@link #getVisibleBounds()}
770      *
771      * @return Rect
772      * @throws UiObjectNotFoundException
773      */
getBounds()774     public @NonNull Rect getBounds() throws UiObjectNotFoundException {
775         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
776         if(node == null) {
777             throw new UiObjectNotFoundException(mUiSelector.toString());
778         }
779         Rect nodeRect = new Rect();
780         node.getBoundsInScreen(nodeRect);
781 
782         return nodeRect;
783     }
784 
785     /**
786      * Waits a specified length of time for a view to become visible.
787      *
788      * This method waits until the view becomes visible on the display, or
789      * until the timeout has elapsed. You can use this method in situations where
790      * the content that you want to select is not immediately displayed.
791      *
792      * @param timeout the amount of time to wait (in milliseconds)
793      * @return true if the view is displayed, else false if timeout elapsed while waiting
794      */
waitForExists(long timeout)795     public boolean waitForExists(long timeout) {
796         Log.d(TAG, String.format("Waiting %dms for %s.", timeout, mUiSelector));
797         return findAccessibilityNodeInfo(timeout) != null;
798     }
799 
800     /**
801      * Waits a specified length of time for a view to become undetectable.
802      *
803      * This method waits until a view is no longer matchable, or until the
804      * timeout has elapsed.
805      *
806      * A view becomes undetectable when the {@link UiSelector} of the object is
807      * unable to find a match because the element has either changed its state or is no
808      * longer displayed.
809      *
810      * You can use this method when attempting to wait for some long operation
811      * to compete, such as downloading a large file or connecting to a remote server.
812      *
813      * @param timeout time to wait (in milliseconds)
814      * @return true if the element is gone before timeout elapsed, else false if timeout elapsed
815      * but a matching element is still found.
816      */
waitUntilGone(long timeout)817     public boolean waitUntilGone(long timeout) {
818         Log.d(TAG, String.format("Waiting %dms for %s to be gone.", timeout, mUiSelector));
819         long startMills = SystemClock.uptimeMillis();
820         long currentMills = 0;
821         while (currentMills <= timeout) {
822             if(findAccessibilityNodeInfo(0) == null)
823                 return true;
824             currentMills = SystemClock.uptimeMillis() - startMills;
825             if(timeout > 0)
826                 SystemClock.sleep(WAIT_FOR_SELECTOR_POLL);
827         }
828         return false;
829     }
830 
831     /**
832      * Check if view exists.
833      *
834      * This methods performs a {@link #waitForExists(long)} with zero timeout. This
835      * basically returns immediately whether the view represented by this UiObject
836      * exists or not. If you need to wait longer for this view, then see
837      * {@link #waitForExists(long)}.
838      *
839      * @return true if the view represented by this UiObject does exist
840      */
exists()841     public boolean exists() {
842         return waitForExists(0);
843     }
844 
safeStringReturn(CharSequence cs)845     private String safeStringReturn(CharSequence cs) {
846         if(cs == null)
847             return "";
848         return cs.toString();
849     }
850 
851     /**
852      * Performs a two-pointer gesture, where each pointer moves diagonally
853      * opposite across the other, from the center out towards the edges of the
854      * this UiObject.
855      * @param percent percentage of the object's diagonal length for the pinch gesture
856      * @param steps the number of steps for the gesture. Steps are injected
857      * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
858      * @return <code>true</code> if all touch events for this gesture are injected successfully,
859      *         <code>false</code> otherwise
860      * @throws UiObjectNotFoundException
861      */
pinchOut(int percent, int steps)862     public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException {
863         if (percent < 0 || percent > 100) {
864             throw new IllegalArgumentException("Percent must be between 0 and 100");
865         }
866         float percentage = percent / 100f;
867 
868         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
869         if (node == null) {
870             throw new UiObjectNotFoundException(mUiSelector.toString());
871         }
872 
873         Rect rect = getVisibleBounds(node);
874         if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
875             throw new IllegalStateException("Object width is too small for operation");
876 
877         // start from the same point at the center of the control
878         Point startPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
879         Point startPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
880 
881         // End at the top-left and bottom-right corners of the control
882         Point endPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
883                 rect.centerY());
884         Point endPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
885                 rect.centerY());
886 
887         return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
888     }
889 
890     /**
891      * Performs a two-pointer gesture, where each pointer moves diagonally
892      * toward the other, from the edges to the center of this UiObject .
893      * @param percent percentage of the object's diagonal length for the pinch gesture
894      * @param steps the number of steps for the gesture. Steps are injected
895      * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
896      * @return <code>true</code> if all touch events for this gesture are injected successfully,
897      *         <code>false</code> otherwise
898      * @throws UiObjectNotFoundException
899      */
pinchIn(int percent, int steps)900     public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException {
901         if (percent < 0 || percent > 100) {
902             throw new IllegalArgumentException("Percent must be between 0 and 100");
903         }
904         float percentage = percent / 100f;
905 
906         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
907         if (node == null) {
908             throw new UiObjectNotFoundException(mUiSelector.toString());
909         }
910 
911         Rect rect = getVisibleBounds(node);
912         if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
913             throw new IllegalStateException("Object width is too small for operation");
914 
915         Point startPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
916                 rect.centerY());
917         Point startPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
918                 rect.centerY());
919 
920         Point endPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
921         Point endPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
922 
923         return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
924     }
925 
926     /**
927      * Generates a two-pointer gesture with arbitrary starting and ending points.
928      *
929      * @param startPoint1 start point of pointer 1
930      * @param startPoint2 start point of pointer 2
931      * @param endPoint1 end point of pointer 1
932      * @param endPoint2 end point of pointer 2
933      * @param steps the number of steps for the gesture. Steps are injected
934      * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
935      * @return <code>true</code> if all touch events for this gesture are injected successfully,
936      *         <code>false</code> otherwise
937      */
performTwoPointerGesture(@onNull Point startPoint1, @NonNull Point startPoint2, @NonNull Point endPoint1, @NonNull Point endPoint2, int steps)938     public boolean performTwoPointerGesture(@NonNull Point startPoint1, @NonNull Point startPoint2,
939             @NonNull Point endPoint1, @NonNull Point endPoint2, int steps) {
940 
941         // avoid a divide by zero
942         if(steps == 0)
943             steps = 1;
944 
945         final float stepX1 = (endPoint1.x - startPoint1.x) / steps;
946         final float stepY1 = (endPoint1.y - startPoint1.y) / steps;
947         final float stepX2 = (endPoint2.x - startPoint2.x) / steps;
948         final float stepY2 = (endPoint2.y - startPoint2.y) / steps;
949 
950         int eventX1, eventY1, eventX2, eventY2;
951         eventX1 = startPoint1.x;
952         eventY1 = startPoint1.y;
953         eventX2 = startPoint2.x;
954         eventY2 = startPoint2.y;
955 
956         // allocate for steps plus first down and last up
957         PointerCoords[] points1 = new PointerCoords[steps + 2];
958         PointerCoords[] points2 = new PointerCoords[steps + 2];
959 
960         // Include the first and last touch downs in the arrays of steps
961         for (int i = 0; i < steps + 1; i++) {
962             PointerCoords p1 = new PointerCoords();
963             p1.x = eventX1;
964             p1.y = eventY1;
965             p1.pressure = 1;
966             p1.size = 1;
967             points1[i] = p1;
968 
969             PointerCoords p2 = new PointerCoords();
970             p2.x = eventX2;
971             p2.y = eventY2;
972             p2.pressure = 1;
973             p2.size = 1;
974             points2[i] = p2;
975 
976             eventX1 = (int) (eventX1 + stepX1);
977             eventY1 = (int) (eventY1 + stepY1);
978             eventX2 = (int) (eventX2 + stepX2);
979             eventY2 = (int) (eventY2 + stepY2);
980         }
981 
982         // ending pointers coordinates
983         PointerCoords p1 = new PointerCoords();
984         p1.x = endPoint1.x;
985         p1.y = endPoint1.y;
986         p1.pressure = 1;
987         p1.size = 1;
988         points1[steps + 1] = p1;
989 
990         PointerCoords p2 = new PointerCoords();
991         p2.x = endPoint2.x;
992         p2.y = endPoint2.y;
993         p2.pressure = 1;
994         p2.size = 1;
995         points2[steps + 1] = p2;
996 
997         return performMultiPointerGesture(points1, points2);
998     }
999 
1000     /**
1001      * Performs a multi-touch gesture. You must specify touch coordinates for
1002      * at least 2 pointers. Each pointer must have all of its touch steps
1003      * defined in an array of {@link PointerCoords}. You can use this method to
1004      * specify complex gestures, like circles and irregular shapes, where each
1005      * pointer may take a different path.
1006      *
1007      * To create a single point on a pointer's touch path:
1008      * <code>
1009      *       PointerCoords p = new PointerCoords();
1010      *       p.x = stepX;
1011      *       p.y = stepY;
1012      *       p.pressure = 1;
1013      *       p.size = 1;
1014      * </code>
1015      * @param touches represents the pointers' paths. Each {@link PointerCoords}
1016      * array represents a different pointer. Each {@link PointerCoords} in an
1017      * array element represents a touch point on a pointer's path.
1018      * @return <code>true</code> if all touch events for this gesture are injected successfully,
1019      *         <code>false</code> otherwise
1020      */
performMultiPointerGesture(PointerCoords @onNull [].... touches)1021     public boolean performMultiPointerGesture(PointerCoords @NonNull []... touches) {
1022         Log.d(TAG, String.format("Performing multi-point gesture %s.", touchesToString(touches)));
1023         return getInteractionController().performMultiPointerGesture(touches);
1024     }
1025 
touchesToString(PointerCoords @onNull [].... touches)1026     private static String touchesToString(PointerCoords @NonNull []... touches) {
1027         StringBuilder result = new StringBuilder();
1028         result.append("[");
1029         for (int i = 0; i < touches.length; i++) {
1030             result.append("[");
1031             for (int j = 0; j < touches[i].length; j++) {
1032                 PointerCoords point = touches[i][j];
1033                 result.append(String.format("(%f, %f)", point.x, point.y));
1034                 if (j + 1 < touches[i].length) result.append(", ");
1035             }
1036             result.append("]");
1037             if (i + 1 < touches.length) result.append(", ");
1038         }
1039         result.append("]");
1040         return result.toString();
1041     }
1042 }
1043