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.accessibilityservice.AccessibilityService;
20 import android.accessibilityservice.AccessibilityServiceInfo;
21 import android.annotation.SuppressLint;
22 import android.app.Instrumentation;
23 import android.app.Service;
24 import android.app.UiAutomation;
25 import android.app.UiAutomation.AccessibilityEventFilter;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.graphics.Bitmap;
31 import android.graphics.Point;
32 import android.hardware.display.DisplayManager;
33 import android.os.Build;
34 import android.os.ParcelFileDescriptor;
35 import android.os.RemoteException;
36 import android.os.SystemClock;
37 import android.util.DisplayMetrics;
38 import android.util.Log;
39 import android.util.SparseArray;
40 import android.view.Display;
41 import android.view.KeyEvent;
42 import android.view.Surface;
43 import android.view.WindowManager;
44 import android.view.accessibility.AccessibilityEvent;
45 import android.view.accessibility.AccessibilityNodeInfo;
46 import android.view.accessibility.AccessibilityWindowInfo;
47 
48 import androidx.annotation.Discouraged;
49 import androidx.annotation.Px;
50 import androidx.annotation.RequiresApi;
51 import androidx.test.uiautomator.util.Traces;
52 import androidx.test.uiautomator.util.Traces.Section;
53 
54 import org.jspecify.annotations.NonNull;
55 import org.jspecify.annotations.Nullable;
56 
57 import java.io.BufferedOutputStream;
58 import java.io.File;
59 import java.io.FileInputStream;
60 import java.io.FileOutputStream;
61 import java.io.IOException;
62 import java.io.OutputStream;
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.HashMap;
66 import java.util.LinkedHashMap;
67 import java.util.LinkedHashSet;
68 import java.util.List;
69 import java.util.Map;
70 import java.util.concurrent.TimeoutException;
71 
72 /**
73  * UiDevice provides access to state information about the device.
74  * You can also use this class to simulate user actions on the device,
75  * such as pressing the d-pad or pressing the Home and Menu buttons.
76  */
77 public class UiDevice implements Searchable {
78 
79     static final String TAG = UiDevice.class.getSimpleName();
80 
81     private static final int MAX_UIAUTOMATION_RETRY = 3;
82     private static final int UIAUTOMATION_RETRY_INTERVAL = 500; // ms
83     // Workaround for stale accessibility cache issues: duration after which the a11y service flags
84     // should be reset (when fetching a UiAutomation instance) to periodically invalidate the cache.
85     private static final long SERVICE_FLAGS_TIMEOUT = 2_000; // ms
86 
87     // Use a short timeout after HOME or BACK key presses, as no events might be generated if
88     // already on the home page or if there is nothing to go back to.
89     private static final long KEY_PRESS_EVENT_TIMEOUT = 1_000; // ms
90     private static final long ROTATION_TIMEOUT = 2_000; // ms
91 
92     // Singleton instance.
93     private static UiDevice sInstance;
94 
95     private final Instrumentation mInstrumentation;
96     private final QueryController mQueryController;
97     private final InteractionController mInteractionController;
98     private final DisplayManager mDisplayManager;
99     private final WaitMixin<UiDevice> mWaitMixin = new WaitMixin<>(this);
100 
101     // Track accessibility service flags to determine when the underlying connection has changed.
102     private int mCachedServiceFlags = -1;
103     private long mLastServiceFlagsTime = -1;
104     private boolean mCompressed = false;
105 
106     // Lazily created UI context per display, used to access UI components/configurations.
107     private final Map<Integer, Context> mUiContexts = new HashMap<>();
108 
109     // Track registered UiWatchers, and whether currently in a UiWatcher execution.
110     private final Map<String, UiWatcher> mWatchers = new LinkedHashMap<>();
111     private final List<String> mWatchersTriggers = new ArrayList<>();
112     private boolean mInWatcherContext = false;
113 
114     /** Private constructor. Clients should use {@link UiDevice#getInstance(Instrumentation)}. */
UiDevice(Instrumentation instrumentation)115     UiDevice(Instrumentation instrumentation) {
116         mInstrumentation = instrumentation;
117         mQueryController = new QueryController(this);
118         mInteractionController = new InteractionController(this);
119         mDisplayManager = (DisplayManager) instrumentation.getContext().getSystemService(
120                 Service.DISPLAY_SERVICE);
121     }
122 
isInWatcherContext()123     boolean isInWatcherContext() {
124         return mInWatcherContext;
125     }
126 
127     /**
128      * Returns a UiObject which represents a view that matches the specified selector criteria.
129      *
130      * @param selector
131      * @return UiObject object
132      */
findObject(@onNull UiSelector selector)133     public @NonNull UiObject findObject(@NonNull UiSelector selector) {
134         return new UiObject(this, selector);
135     }
136 
137     /** Returns whether there is a match for the given {@code selector} criteria. */
138     @Override
hasObject(@onNull BySelector selector)139     public boolean hasObject(@NonNull BySelector selector) {
140         Log.d(TAG, String.format("Searching for node with selector: %s.", selector));
141         AccessibilityNodeInfo node = ByMatcher.findMatch(
142                 this,
143                 selector,
144                 getWindowRoots().toArray(new AccessibilityNodeInfo[0])
145         );
146         if (node != null) {
147             node.recycle();
148             return true;
149         }
150         return false;
151     }
152 
153     /**
154      * Returns the first object to match the {@code selector} criteria,
155      * or null if no matching objects are found.
156      */
157     @Override
158     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
findObject(@onNull BySelector selector)159     public UiObject2 findObject(@NonNull BySelector selector) {
160         Log.d(TAG, String.format("Retrieving node with selector: %s.", selector));
161         AccessibilityNodeInfo node = ByMatcher.findMatch(
162                 this,
163                 selector,
164                 getWindowRoots().toArray(new AccessibilityNodeInfo[0])
165         );
166         if (node == null) {
167             Log.d(TAG, String.format("Node not found with selector: %s.", selector));
168             return null;
169         }
170         return UiObject2.create(this, selector, node);
171     }
172 
173     /** Returns all objects that match the {@code selector} criteria. */
174     @Override
findObjects(@onNull BySelector selector)175     public @NonNull List<UiObject2> findObjects(@NonNull BySelector selector) {
176         Log.d(TAG, String.format("Retrieving nodes with selector: %s.", selector));
177         List<UiObject2> ret = new ArrayList<>();
178         for (AccessibilityNodeInfo node : ByMatcher.findMatches(
179                 this,
180                 selector,
181                 getWindowRoots().toArray(new AccessibilityNodeInfo[0]))
182         ) {
183             UiObject2 object = UiObject2.create(this, selector, node);
184             if (object != null) {
185                 ret.add(object);
186             }
187         }
188         return ret;
189     }
190 
191 
192     /**
193      * Waits for given the {@code condition} to be met.
194      *
195      * @param condition The {@link SearchCondition} to evaluate.
196      * @param timeout Maximum amount of time to wait in milliseconds.
197      * @return The final result returned by the {@code condition}, or null if the {@code condition}
198      * was not met before the {@code timeout}.
199      */
wait(@onNull SearchCondition<U> condition, long timeout)200     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
201         return wait((Condition<? super UiDevice, U>) condition, timeout);
202     }
203 
204     /**
205      * Waits for given the {@code condition} to be met.
206      *
207      * @param condition The {@link Condition} to evaluate.
208      * @param timeout Maximum amount of time to wait in milliseconds.
209      * @return The final result returned by the {@code condition}, or null if the {@code condition}
210      * was not met before the {@code timeout}.
211      */
wait(@onNull Condition<? super UiDevice, U> condition, long timeout)212     public <U> U wait(@NonNull Condition<? super UiDevice, U> condition, long timeout) {
213         try (Section ignored = Traces.trace("UiDevice#wait")) {
214             Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
215             return mWaitMixin.wait(condition, timeout);
216         }
217     }
218 
219     /**
220      * Performs the provided {@code action} and waits for the {@code condition} to be met.
221      *
222      * @param action The {@link Runnable} action to perform.
223      * @param condition The {@link EventCondition} to evaluate.
224      * @param timeout Maximum amount of time to wait in milliseconds.
225      * @return The final result returned by the condition.
226      */
performActionAndWait(@onNull Runnable action, @NonNull EventCondition<U> condition, long timeout)227     public <U> U performActionAndWait(@NonNull Runnable action,
228             @NonNull EventCondition<U> condition, long timeout) {
229         try (Section ignored = Traces.trace("UiDevice#performActionAndWait")) {
230             AccessibilityEvent event = null;
231             Log.d(TAG, String.format("Performing action %s and waiting %dms for %s.", action,
232                     timeout, condition));
233             try {
234                 event = getUiAutomation().executeAndWaitForEvent(
235                         action, condition, timeout);
236             } catch (TimeoutException e) {
237                 // Ignore
238                 Log.w(TAG, String.format("Timed out waiting %dms on the condition.", timeout), e);
239             }
240 
241             if (event != null) {
242                 event.recycle();
243             }
244 
245             return condition.getResult();
246         }
247     }
248 
249     /**
250      * Enables or disables layout hierarchy compression.
251      *
252      * If compression is enabled, the layout hierarchy derived from the Acessibility
253      * framework will only contain nodes that are important for uiautomator
254      * testing. Any unnecessary surrounding layout nodes that make viewing
255      * and searching the hierarchy inefficient are removed.
256      *
257      * @param compressed true to enable compression; else, false to disable
258      * @deprecated Typo in function name, should use {@link #setCompressedLayoutHierarchy(boolean)}
259      * instead.
260      */
261     @Deprecated
setCompressedLayoutHeirarchy(boolean compressed)262     public void setCompressedLayoutHeirarchy(boolean compressed) {
263         this.setCompressedLayoutHierarchy(compressed);
264     }
265 
266     /**
267      * Enables or disables layout hierarchy compression.
268      *
269      * If compression is enabled, the layout hierarchy derived from the Accessibility
270      * framework will only contain nodes that are important for uiautomator
271      * testing. Any unnecessary surrounding layout nodes that make viewing
272      * and searching the hierarchy inefficient are removed.
273      *
274      * @param compressed true to enable compression; else, false to disable
275      */
setCompressedLayoutHierarchy(boolean compressed)276     public void setCompressedLayoutHierarchy(boolean compressed) {
277         mCompressed = compressed;
278         mCachedServiceFlags = -1; // Reset cached accessibility service flags to force an update.
279     }
280 
281     /**
282      * Retrieves a singleton instance of UiDevice
283      *
284      * @deprecated Should use {@link #getInstance(Instrumentation)} instead. This version hides
285      * UiDevice's dependency on having an Instrumentation reference and is prone to misuse.
286      * @return UiDevice instance
287      */
288     @Deprecated
getInstance()289     public static @NonNull UiDevice getInstance() {
290         if (sInstance == null) {
291             throw new IllegalStateException("UiDevice singleton not initialized");
292         }
293         return sInstance;
294     }
295 
296     /**
297      * Retrieves a singleton instance of UiDevice. A new instance will be created if
298      * instrumentation is also new.
299      *
300      * @return UiDevice instance
301      */
getInstance(@onNull Instrumentation instrumentation)302     public static @NonNull UiDevice getInstance(@NonNull Instrumentation instrumentation) {
303         if (sInstance == null || !instrumentation.equals(sInstance.mInstrumentation)) {
304             Log.i(TAG, String.format("Creating a new instance, old instance exists: %b",
305                     (sInstance != null)));
306             sInstance = new UiDevice(instrumentation);
307         }
308         return sInstance;
309     }
310 
311     /**
312      * Returns the default display size in dp (device-independent pixel).
313      * <p>The returned display size is adjusted per screen rotation. Also this will return the
314      * actual size of the screen, rather than adjusted per system decorations (like status bar).
315      *
316      * @see DisplayMetrics#density
317      * @return a Point containing the display size in dp
318      */
getDisplaySizeDp()319     public @NonNull Point getDisplaySizeDp() {
320         Point p = getDisplaySize(Display.DEFAULT_DISPLAY);
321         Context context = getUiContext(Display.DEFAULT_DISPLAY);
322         int densityDpi = context.getResources().getConfiguration().densityDpi;
323         float density = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
324         return new Point(Math.round(p.x / density), Math.round(p.y / density));
325     }
326 
327     /**
328      * Retrieves the product name of the device.
329      *
330      * This method provides information on what type of device the test is running on. This value is
331      * the same as returned by invoking #adb shell getprop ro.product.name.
332      *
333      * @return product name of the device
334      */
getProductName()335     public @NonNull String getProductName() {
336         return Build.PRODUCT;
337     }
338 
339     /**
340      * Retrieves the text from the last UI traversal event received.
341      *
342      * You can use this method to read the contents in a WebView container
343      * because the accessibility framework fires events
344      * as each text is highlighted. You can write a test to perform
345      * directional arrow presses to focus on different elements inside a WebView,
346      * and call this method to get the text from each traversed element.
347      * If you are testing a view container that can return a reference to a
348      * Document Object Model (DOM) object, your test should use the view's
349      * DOM instead.
350      *
351      * @return text of the last traversal event, else return an empty string
352      */
353     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
getLastTraversedText()354     public String getLastTraversedText() {
355         return getQueryController().getLastTraversedText();
356     }
357 
358     /**
359      * Clears the text from the last UI traversal event.
360      * See {@link #getLastTraversedText()}.
361      */
clearLastTraversedText()362     public void clearLastTraversedText() {
363         Log.d(TAG, "Clearing last traversed text.");
364         getQueryController().clearLastTraversedText();
365     }
366 
367     /**
368      * Simulates a short press on the MENU button.
369      * @return true if successful, else return false
370      */
pressMenu()371     public boolean pressMenu() {
372         waitForIdle();
373         Log.d(TAG, "Pressing menu button.");
374         return getInteractionController().sendKeyAndWaitForEvent(
375                 KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
376                 KEY_PRESS_EVENT_TIMEOUT);
377     }
378 
379     /**
380      * Simulates a short press on the BACK button.
381      * @return true if successful, else return false
382      */
pressBack()383     public boolean pressBack() {
384         waitForIdle();
385         Log.d(TAG, "Pressing back button.");
386         return getInteractionController().sendKeyAndWaitForEvent(
387                 KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
388                 KEY_PRESS_EVENT_TIMEOUT);
389     }
390 
391     /**
392      * Simulates a short press on the HOME button.
393      * @return true if successful, else return false
394      */
pressHome()395     public boolean pressHome() {
396         waitForIdle();
397         Log.d(TAG, "Pressing home button.");
398         return getInteractionController().sendKeyAndWaitForEvent(
399                 KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
400                 KEY_PRESS_EVENT_TIMEOUT);
401     }
402 
403     /**
404      * Simulates a short press on the SEARCH button.
405      * @return true if successful, else return false
406      */
pressSearch()407     public boolean pressSearch() {
408         return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
409     }
410 
411     /**
412      * Simulates a short press on the CENTER button.
413      * @return true if successful, else return false
414      */
pressDPadCenter()415     public boolean pressDPadCenter() {
416         return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
417     }
418 
419     /**
420      * Simulates a short press on the DOWN button.
421      * @return true if successful, else return false
422      */
pressDPadDown()423     public boolean pressDPadDown() {
424         return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
425     }
426 
427     /**
428      * Simulates a short press on the UP button.
429      * @return true if successful, else return false
430      */
pressDPadUp()431     public boolean pressDPadUp() {
432         return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
433     }
434 
435     /**
436      * Simulates a short press on the LEFT button.
437      * @return true if successful, else return false
438      */
pressDPadLeft()439     public boolean pressDPadLeft() {
440         return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
441     }
442 
443     /**
444      * Simulates a short press on the RIGHT button.
445      * @return true if successful, else return false
446      */
pressDPadRight()447     public boolean pressDPadRight() {
448         return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
449     }
450 
451     /**
452      * Simulates a short press on the DELETE key.
453      * @return true if successful, else return false
454      */
pressDelete()455     public boolean pressDelete() {
456         return pressKeyCode(KeyEvent.KEYCODE_DEL);
457     }
458 
459     /**
460      * Simulates a short press on the ENTER key.
461      * @return true if successful, else return false
462      */
pressEnter()463     public boolean pressEnter() {
464         return pressKeyCode(KeyEvent.KEYCODE_ENTER);
465     }
466 
467     /**
468      * Simulates a short press using a key code.
469      *
470      * See {@link KeyEvent}
471      * @return true if successful, else return false
472      */
pressKeyCode(int keyCode)473     public boolean pressKeyCode(int keyCode) {
474         return pressKeyCode(keyCode, 0);
475     }
476 
477     /**
478      * Simulates a short press using a key code.
479      *
480      * See {@link KeyEvent}.
481      * @param keyCode the key code of the event.
482      * @param metaState an integer in which each bit set to 1 represents a pressed meta key
483      * @return true if successful, else return false
484      */
pressKeyCode(int keyCode, int metaState)485     public boolean pressKeyCode(int keyCode, int metaState) {
486         return pressKeyCodes(new int[]{keyCode}, metaState);
487     }
488 
489     /**
490      * Presses one or more keys. Keys that change meta state are supported, and will apply their
491      * meta state to following keys.
492      * <br/>
493      * For example, you can simulate taking a screenshot on the device by pressing both the
494      * power and volume down keys.
495      * <pre>{@code pressKeyCodes(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_VOLUME_DOWN})}
496      * </pre>
497      *
498      * @see KeyEvent
499      * @param keyCodes array of key codes.
500      * @return true if successful, else return false
501      */
pressKeyCodes(int @NonNull [] keyCodes)502     public boolean pressKeyCodes(int @NonNull [] keyCodes) {
503         return pressKeyCodes(keyCodes, 0);
504     }
505 
506     /**
507      * Presses one or more keys. Keys that change meta state are supported, and will apply their
508      * meta state to following keys.
509      * <br/>
510      * For example, you can simulate taking a screenshot on the device by pressing both the
511      * power and volume down keys.
512      * <pre>{@code pressKeyCodes(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_VOLUME_DOWN})}
513      * </pre>
514      *
515      * @see KeyEvent
516      * @param keyCodes array of key codes.
517      * @param metaState an integer in which each bit set to 1 represents a pressed meta key
518      * @return true if successful, else return false
519      */
pressKeyCodes(int @NonNull [] keyCodes, int metaState)520     public boolean pressKeyCodes(int @NonNull [] keyCodes, int metaState) {
521         waitForIdle();
522         Log.d(TAG, String.format("Pressing keycodes %s with modifier %d.",
523                 Arrays.toString(keyCodes),
524                 metaState));
525         return getInteractionController().sendKeys(keyCodes, metaState);
526     }
527 
528     /**
529      * Simulates a short press on the Recent Apps button.
530      *
531      * @return true if successful, else return false
532      * @throws RemoteException never
533      */
pressRecentApps()534     public boolean pressRecentApps() throws RemoteException {
535         waitForIdle();
536         Log.d(TAG, "Pressing recent apps button.");
537         return getUiAutomation().performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
538     }
539 
540     /**
541      * Opens the notification shade.
542      *
543      * @return true if successful, else return false
544      */
openNotification()545     public boolean openNotification() {
546         waitForIdle();
547         Log.d(TAG, "Opening notification.");
548         return getUiAutomation().performGlobalAction(
549                 AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
550     }
551 
552     /**
553      * Opens the Quick Settings shade.
554      *
555      * @return true if successful, else return false
556      */
openQuickSettings()557     public boolean openQuickSettings() {
558         waitForIdle();
559         Log.d(TAG, "Opening quick settings.");
560         return getUiAutomation().performGlobalAction(
561                 AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
562     }
563 
564     /**
565      * Gets the width of the default display, in pixels. The size is adjusted based on the
566      * current orientation of the display.
567      *
568      * @return width in pixels
569      */
getDisplayWidth()570     public @Px int getDisplayWidth() {
571         return getDisplayWidth(Display.DEFAULT_DISPLAY);
572     }
573 
574     /**
575      * Gets the width of the display with {@code displayId}, in pixels. The size is adjusted
576      * based on the current orientation of the display.
577      *
578      * @param displayId the display ID. Use {@link Display#getDisplayId()} to get the ID.
579      * @return width in pixels
580      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
581      */
getDisplayWidth(int displayId)582     public @Px int getDisplayWidth(int displayId) {
583         return getDisplaySize(displayId).x;
584     }
585 
586     /**
587      * Gets the height of the default display, in pixels. The size is adjusted based on the
588      * current orientation of the display.
589      *
590      * @return height in pixels
591      */
getDisplayHeight()592     public @Px int getDisplayHeight() {
593         return getDisplayHeight(Display.DEFAULT_DISPLAY);
594     }
595 
596     /**
597      * Gets the height of the display with {@code displayId}, in pixels. The size is adjusted
598      * based on the current orientation of the display.
599      *
600      * @param displayId the display ID. Use {@link Display#getDisplayId()} to get the ID.
601      * @return height in pixels
602      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
603      */
getDisplayHeight(int displayId)604     public @Px int getDisplayHeight(int displayId) {
605         return getDisplaySize(displayId).y;
606     }
607 
608     /**
609      * Perform a click at arbitrary coordinates on the default display specified by the user.
610      *
611      * @param x coordinate
612      * @param y coordinate
613      * @return true if the click succeeded else false
614      */
click(int x, int y)615     public boolean click(int x, int y) {
616         if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
617             Log.w(TAG, String.format("Cannot click. Point (%d, %d) is outside display (%d, %d).",
618                     x, y, getDisplayWidth(), getDisplayHeight()));
619             return false;
620         }
621         Log.d(TAG, String.format("Clicking on (%d, %d).", x, y));
622         return getInteractionController().clickNoSync(x, y);
623     }
624 
625     /**
626      * Performs a swipe from one coordinate to another on the default display using the number of
627      * steps to determine smoothness and speed. Each step execution is throttled to 5ms per step.
628      * So for a 100 steps, the swipe will take about 1/2 second to complete.
629      *
630      * @param startX X-axis value for the starting coordinate
631      * @param startY Y-axis value for the starting coordinate
632      * @param endX X-axis value for the ending coordinate
633      * @param endY Y-axis value for the ending coordinate
634      * @param steps is the number of move steps sent to the system
635      * @return false if the operation fails or the coordinates are invalid
636      */
swipe(int startX, int startY, int endX, int endY, int steps)637     public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
638         Log.d(TAG, String.format("Swiping from (%d, %d) to (%d, %d) in %d steps.", startX, startY,
639                 endX, endY, steps));
640         return getInteractionController()
641                 .swipe(startX, startY, endX, endY, steps);
642     }
643 
644     /**
645      * Performs a swipe from one coordinate to another coordinate on the default display. You can
646      * control the smoothness and speed of the swipe by specifying the number of steps. Each step
647      * execution is throttled to 5 milliseconds per step, so for a 100 steps, the swipe will take
648      * around 0.5 seconds to complete.
649      *
650      * @param startX X-axis value for the starting coordinate
651      * @param startY Y-axis value for the starting coordinate
652      * @param endX X-axis value for the ending coordinate
653      * @param endY Y-axis value for the ending coordinate
654      * @param steps is the number of steps for the swipe action
655      * @return true if swipe is performed, false if the operation fails or the coordinates are
656      * invalid
657      */
drag(int startX, int startY, int endX, int endY, int steps)658     public boolean drag(int startX, int startY, int endX, int endY, int steps) {
659         Log.d(TAG, String.format("Dragging from (%d, %d) to (%d, %d) in %d steps.", startX, startY,
660                 endX, endY, steps));
661         return getInteractionController()
662                 .swipe(startX, startY, endX, endY, steps, true);
663     }
664 
665     /**
666      * Performs a swipe between points in the Point array on the default display. Each step
667      * execution is throttled to 5ms per step. So for a 100 steps, the swipe will take about 1/2
668      * second to complete.
669      *
670      * @param segments is Point array containing at least one Point object
671      * @param segmentSteps steps to inject between two Points
672      * @return true on success
673      */
swipe(Point @onNull [] segments, int segmentSteps)674     public boolean swipe(Point @NonNull [] segments, int segmentSteps) {
675         Log.d(TAG, String.format("Swiping between %s in %d steps.", Arrays.toString(segments),
676                 segmentSteps * (segments.length - 1)));
677         return getInteractionController().swipe(segments, segmentSteps);
678     }
679 
680     /**
681      * Waits for the current application to idle.
682      * Default wait timeout is 10 seconds
683      */
waitForIdle()684     public void waitForIdle() {
685         try (Section ignored = Traces.trace("UiDevice#waitForIdle")) {
686             getQueryController().waitForIdle();
687         }
688     }
689 
690     /**
691      * Waits for the current application to idle.
692      * @param timeout in milliseconds
693      */
waitForIdle(long timeout)694     public void waitForIdle(long timeout) {
695         try (Section ignored = Traces.trace("UiDevice#waitForIdle")) {
696             getQueryController().waitForIdle(timeout);
697         }
698     }
699 
700     /**
701      * Retrieves the last activity to report accessibility events.
702      * @deprecated The results returned should be considered unreliable
703      * @return String name of activity
704      */
705     @Deprecated
706     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
getCurrentActivityName()707     public String getCurrentActivityName() {
708         return getQueryController().getCurrentActivityName();
709     }
710 
711     /**
712      * Retrieves the name of the last package to report accessibility events.
713      * @return String name of package
714      */
715     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
getCurrentPackageName()716     public String getCurrentPackageName() {
717         return getQueryController().getCurrentPackageName();
718     }
719 
720     /**
721      * Registers a {@link UiWatcher} to run automatically when the testing framework is unable to
722      * find a match using a {@link UiSelector}. See {@link #runWatchers()}
723      *
724      * @param name to register the UiWatcher
725      * @param watcher {@link UiWatcher}
726      */
registerWatcher(@ullable String name, @Nullable UiWatcher watcher)727     public void registerWatcher(@Nullable String name, @Nullable UiWatcher watcher) {
728         Log.d(TAG, String.format("Registering watcher %s.", name));
729         if (mInWatcherContext) {
730             throw new IllegalStateException("Cannot register new watcher from within another");
731         }
732         mWatchers.put(name, watcher);
733     }
734 
735     /**
736      * Removes a previously registered {@link UiWatcher}.
737      *
738      * See {@link #registerWatcher(String, UiWatcher)}
739      * @param name used to register the UiWatcher
740      */
removeWatcher(@ullable String name)741     public void removeWatcher(@Nullable String name) {
742         Log.d(TAG, String.format("Removing watcher %s.", name));
743         if (mInWatcherContext) {
744             throw new IllegalStateException("Cannot remove a watcher from within another");
745         }
746         mWatchers.remove(name);
747     }
748 
749     /**
750      * This method forces all registered watchers to run.
751      * See {@link #registerWatcher(String, UiWatcher)}
752      */
runWatchers()753     public void runWatchers() {
754         if (mInWatcherContext) {
755             return;
756         }
757 
758         for (String watcherName : mWatchers.keySet()) {
759             UiWatcher watcher = mWatchers.get(watcherName);
760             if (watcher != null) {
761                 try {
762                     mInWatcherContext = true;
763                     if (watcher.checkForCondition()) {
764                         setWatcherTriggered(watcherName);
765                     }
766                 } catch (Exception e) {
767                     Log.e(TAG, String.format("Failed to execute watcher %s.", watcherName), e);
768                 } finally {
769                     mInWatcherContext = false;
770                 }
771             }
772         }
773     }
774 
775     /**
776      * Resets a {@link UiWatcher} that has been triggered.
777      * If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call
778      * returned <code>true</code>, then the UiWatcher is considered triggered.
779      * See {@link #registerWatcher(String, UiWatcher)}
780      */
resetWatcherTriggers()781     public void resetWatcherTriggers() {
782         Log.d(TAG, "Resetting all watchers.");
783         mWatchersTriggers.clear();
784     }
785 
786     /**
787      * Checks if a specific registered  {@link UiWatcher} has triggered.
788      * See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its
789      * {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then
790      * the UiWatcher is considered triggered. This is helpful if a watcher is detecting errors
791      * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered.
792      *
793      * @param watcherName
794      * @return true if triggered else false
795      */
hasWatcherTriggered(@ullable String watcherName)796     public boolean hasWatcherTriggered(@Nullable String watcherName) {
797         return mWatchersTriggers.contains(watcherName);
798     }
799 
800     /**
801      * Checks if any registered {@link UiWatcher} have triggered.
802      *
803      * See {@link #registerWatcher(String, UiWatcher)}
804      * See {@link #hasWatcherTriggered(String)}
805      */
hasAnyWatcherTriggered()806     public boolean hasAnyWatcherTriggered() {
807         return mWatchersTriggers.size() > 0;
808     }
809 
810     /**
811      * Used internally by this class to set a {@link UiWatcher} state as triggered.
812      * @param watcherName
813      */
setWatcherTriggered(String watcherName)814     private void setWatcherTriggered(String watcherName) {
815         if (!hasWatcherTriggered(watcherName)) {
816             mWatchersTriggers.add(watcherName);
817         }
818     }
819 
820     /**
821      * @return true if default display is in its natural or flipped (180 degrees) orientation
822      */
isNaturalOrientation()823     public boolean isNaturalOrientation() {
824         return isNaturalOrientation(Display.DEFAULT_DISPLAY);
825     }
826 
827     /**
828      * @return true if display with {@code displayId} is in its natural or flipped (180 degrees)
829      * orientation
830      * @see Display#getDisplayId()
831      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
832      */
isNaturalOrientation(int displayId)833     private boolean isNaturalOrientation(int displayId) {
834         int ret = getDisplayRotation(displayId);
835         return ret == UiAutomation.ROTATION_FREEZE_0
836                 || ret == UiAutomation.ROTATION_FREEZE_180;
837     }
838 
839     /**
840      * @return the current rotation of the default display
841      * @see Display#getRotation()
842      */
getDisplayRotation()843     public int getDisplayRotation() {
844         return getDisplayRotation(Display.DEFAULT_DISPLAY);
845     }
846 
847     /**
848      * @return the current rotation of the display with {@code displayId}
849      * @see Display#getDisplayId()
850      * @see Display#getRotation()
851      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
852      */
getDisplayRotation(int displayId)853     public int getDisplayRotation(int displayId) {
854         waitForIdle();
855         Display display = getDisplayById(displayId);
856         if (display == null) {
857             throw new IllegalArgumentException(String.format("Display %d not found or not "
858                     + "accessible", displayId));
859         }
860         return display.getRotation();
861     }
862 
863     /**
864      * Freezes the default display rotation at its current state.
865      * @throws RemoteException never
866      */
freezeRotation()867     public void freezeRotation() throws RemoteException {
868         Log.d(TAG, "Freezing rotation.");
869         getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
870     }
871 
872     /**
873      * Freezes the rotation of the display with {@code displayId} at its current state.
874      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
875      * officially supported.
876      * @see Display#getDisplayId()
877      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
878      */
879     @RequiresApi(30)
freezeRotation(int displayId)880     public void freezeRotation(int displayId) {
881         Log.d(TAG, String.format("Freezing rotation on display %d.", displayId));
882         try {
883             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
884                 executeShellCommand(String.format("cmd window user-rotation -d %d lock",
885                         displayId));
886             } else {
887                 int rotation = getDisplayRotation(displayId);
888                 executeShellCommand(String.format("cmd window set-user-rotation lock -d %d %d",
889                         displayId, rotation));
890             }
891         } catch (IOException e) {
892             throw new RuntimeException(e);
893         }
894     }
895 
896     /**
897      * Un-freezes the default display rotation allowing its contents to rotate with its physical
898      * rotation. During testing, it is best to keep the default display frozen in a specific
899      * orientation.
900      * <p>Note: Need to wait a short period for the rotation animation to complete before
901      * performing another operation.
902      * @throws RemoteException never
903      */
unfreezeRotation()904     public void unfreezeRotation() throws RemoteException {
905         Log.d(TAG, "Unfreezing rotation.");
906         getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
907     }
908 
909     /**
910      * Un-freezes the rotation of the display with {@code displayId} allowing its contents to
911      * rotate with its physical rotation. During testing, it is best to keep the display frozen
912      * in a specific orientation.
913      * <p>Note: Need to wait a short period for the rotation animation to complete before
914      * performing another operation.
915      * <p>Note: Some secondary displays don't have rotation sensors and therefore won't respond
916      * to this method.
917      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
918      * officially supported.
919      * @see Display#getDisplayId()
920      */
921     @RequiresApi(30)
unfreezeRotation(int displayId)922     public void unfreezeRotation(int displayId) {
923         Log.d(TAG, String.format("Unfreezing rotation on display %d.", displayId));
924         try {
925             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
926                 executeShellCommand(String.format("cmd window user-rotation -d %d free",
927                         displayId));
928             } else {
929                 executeShellCommand(String.format("cmd window set-user-rotation free -d %d",
930                         displayId));
931             }
932         } catch (IOException e) {
933             throw new RuntimeException(e);
934         }
935     }
936 
937     /**
938      * Orients the default display to the left and freezes rotation. Use
939      * {@link #unfreezeRotation()} to un-freeze the rotation.
940      * <p>Note: This rotation is relative to the natural orientation which depends on the device
941      * type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
942      * {@link #setOrientationLandscape()}.
943      * @throws RemoteException never
944      */
setOrientationLeft()945     public void setOrientationLeft() throws RemoteException {
946         Log.d(TAG, "Setting orientation to left.");
947         rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
948     }
949 
950     /**
951      * Orients the display with {@code displayId} to the left and freezes rotation. Use
952      * {@link #unfreezeRotation()} to un-freeze the rotation.
953      * <p>Note: This rotation is relative to the natural orientation which depends on the device
954      * type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
955      * {@link #setOrientationLandscape()}.
956      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
957      * officially supported.
958      * @see Display#getDisplayId()
959      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
960      */
961     @RequiresApi(30)
setOrientationLeft(int displayId)962     public void setOrientationLeft(int displayId) {
963         Log.d(TAG, String.format("Setting orientation to left on display %d.", displayId));
964         rotateWithCommand(Surface.ROTATION_90, displayId);
965     }
966 
967     /**
968      * Orients the default display to the right and freezes rotation. Use
969      * {@link #unfreezeRotation()} to un-freeze the rotation.
970      * <p>Note: This rotation is relative to the natural orientation which depends on the device
971      * type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
972      * {@link #setOrientationLandscape()}.
973      * @throws RemoteException never
974      */
setOrientationRight()975     public void setOrientationRight() throws RemoteException {
976         Log.d(TAG, "Setting orientation to right.");
977         rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_270);
978     }
979 
980     /**
981      * Orients the display with {@code displayId} to the right and freezes rotation. Use
982      * {@link #unfreezeRotation()} to un-freeze the rotation.
983      * <p>Note: This rotation is relative to the natural orientation which depends on the device
984      * type (e.g. phone vs. tablet). Consider using {@link #setOrientationPortrait()} and
985      * {@link #setOrientationLandscape()}.
986      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
987      * officially supported.
988      * @see Display#getDisplayId()
989      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
990      */
991     @RequiresApi(30)
setOrientationRight(int displayId)992     public void setOrientationRight(int displayId) {
993         Log.d(TAG, String.format("Setting orientation to right on display %d.", displayId));
994         rotateWithCommand(Surface.ROTATION_270, displayId);
995     }
996 
997     /**
998      * Orients the default display to its natural orientation and freezes rotation. Use
999      * {@link #unfreezeRotation()} to un-freeze the rotation.
1000      * <p>Note: The natural orientation depends on the device type (e.g. phone vs. tablet).
1001      * Consider using {@link #setOrientationPortrait()} and {@link #setOrientationLandscape()}.
1002      * @throws RemoteException never
1003      */
setOrientationNatural()1004     public void setOrientationNatural() throws RemoteException {
1005         Log.d(TAG, "Setting orientation to natural.");
1006         rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
1007     }
1008 
1009     /**
1010      * Orients the display with {@code displayId} to its natural orientation and freezes rotation
1011      * . Use {@link #unfreezeRotation()} to un-freeze the rotation.
1012      * <p>Note: The natural orientation depends on the device type (e.g. phone vs. tablet).
1013      * Consider using {@link #setOrientationPortrait()} and {@link #setOrientationLandscape()}.
1014      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
1015      * officially supported.
1016      * @see Display#getDisplayId()
1017      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1018      */
1019     @RequiresApi(30)
setOrientationNatural(int displayId)1020     public void setOrientationNatural(int displayId) {
1021         Log.d(TAG, String.format("Setting orientation to natural on display %d.", displayId));
1022         rotateWithCommand(Surface.ROTATION_0, displayId);
1023     }
1024 
1025     /**
1026      * Orients the default display to its portrait orientation (height >= width) and freezes
1027      * rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
1028      * @throws RemoteException never
1029      */
setOrientationPortrait()1030     public void setOrientationPortrait() throws RemoteException {
1031         Log.d(TAG, "Setting orientation to portrait.");
1032         if (getDisplayHeight() >= getDisplayWidth()) {
1033             freezeRotation(); // Already in portrait orientation.
1034         } else if (isNaturalOrientation()) {
1035             rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
1036         } else {
1037             rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
1038         }
1039     }
1040 
1041     /**
1042      * Orients the display with {@code displayId} to its portrait orientation (height >= width) and
1043      * freezes rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
1044      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
1045      * officially supported.
1046      * @see Display#getDisplayId()
1047      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1048      */
1049     @RequiresApi(30)
setOrientationPortrait(int displayId)1050     public void setOrientationPortrait(int displayId) {
1051         Log.d(TAG, String.format("Setting orientation to portrait on display %d.", displayId));
1052         if (getDisplayHeight(displayId) >= getDisplayWidth(displayId)) {
1053             freezeRotation(displayId); // Already in portrait orientation.
1054         } else if (isNaturalOrientation(displayId)) {
1055             rotateWithCommand(Surface.ROTATION_90, displayId);
1056         } else {
1057             rotateWithCommand(Surface.ROTATION_0, displayId);
1058         }
1059     }
1060 
1061     /**
1062      * Orients the default display to its landscape orientation (width >= height) and freezes
1063      * rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
1064      * @throws RemoteException never
1065      */
setOrientationLandscape()1066     public void setOrientationLandscape() throws RemoteException {
1067         Log.d(TAG, "Setting orientation to landscape.");
1068         if (getDisplayWidth() >= getDisplayHeight()) {
1069             freezeRotation(); // Already in landscape orientation.
1070         } else if (isNaturalOrientation()) {
1071             rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_90);
1072         } else {
1073             rotateWithUiAutomation(UiAutomation.ROTATION_FREEZE_0);
1074         }
1075     }
1076 
1077     /**
1078      * Orients the display with {@code displayId} to its landscape orientation (width >= height) and
1079      * freezes rotation. Use {@link #unfreezeRotation()} to un-freeze the rotation.
1080      * <p>Note: Only works on Android API level 30 (R) or above, where multi-display is
1081      * officially supported.
1082      * @see Display#getDisplayId()
1083      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1084      */
1085     @RequiresApi(30)
setOrientationLandscape(int displayId)1086     public void setOrientationLandscape(int displayId) {
1087         Log.d(TAG, String.format("Setting orientation to landscape on display %d.", displayId));
1088         if (getDisplayWidth(displayId) >= getDisplayHeight(displayId)) {
1089             freezeRotation(displayId); // Already in landscape orientation.
1090         } else if (isNaturalOrientation(displayId)) {
1091             rotateWithCommand(Surface.ROTATION_90, displayId);
1092         } else {
1093             rotateWithCommand(Surface.ROTATION_0, displayId);
1094         }
1095     }
1096 
1097     /** Rotates the default display using UiAutomation and waits for the rotation to be detected. */
rotateWithUiAutomation(int rotation)1098     private void rotateWithUiAutomation(int rotation) {
1099         getUiAutomation().setRotation(rotation);
1100         waitRotationComplete(rotation, Display.DEFAULT_DISPLAY);
1101     }
1102 
1103     /**
1104      * Rotates the display using shell command and waits for the rotation to be detected.
1105      *
1106      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1107      */
1108     @RequiresApi(30)
rotateWithCommand(int rotation, int displayId)1109     private void rotateWithCommand(int rotation, int displayId) {
1110         try {
1111             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1112                 executeShellCommand(String.format("cmd window user-rotation -d %d lock %d",
1113                         displayId, rotation));
1114             } else {
1115                 executeShellCommand(String.format("cmd window set-user-rotation lock -d %d %d",
1116                         displayId, rotation));
1117             }
1118         } catch (IOException e) {
1119             throw new RuntimeException(e);
1120         }
1121         waitRotationComplete(rotation, displayId);
1122     }
1123 
1124     /**
1125      * Waits for the display with {@code displayId} to be in {@code rotation}.
1126      *
1127      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1128      */
waitRotationComplete(int rotation, int displayId)1129     private void waitRotationComplete(int rotation, int displayId) {
1130         Condition<UiDevice, Boolean> rotationCondition = new Condition<UiDevice, Boolean>() {
1131             @Override
1132             public Boolean apply(UiDevice device) {
1133                 return device.getDisplayRotation(displayId) == rotation;
1134             }
1135 
1136             @Override
1137             public @NonNull String toString() {
1138                 return String.format("Condition[displayRotation=%d, displayId=%d]", rotation,
1139                         displayId);
1140             }
1141         };
1142         if (!wait(rotationCondition, ROTATION_TIMEOUT)) {
1143             Log.w(TAG, String.format("Didn't detect rotation within %dms.", ROTATION_TIMEOUT));
1144         }
1145     }
1146 
1147     /**
1148      * This method simulates pressing the power button if the default display is OFF, else it does
1149      * nothing if the default display is already ON.
1150      * <p>If the default display was OFF and it just got turned ON, this method will insert a 500ms
1151      * delay for the device to wake up and accept input.
1152      *
1153      * @throws RemoteException
1154      */
wakeUp()1155     public void wakeUp() throws RemoteException {
1156         Log.d(TAG, "Turning on screen.");
1157         if(getInteractionController().wakeDevice()) {
1158             // Sync delay to allow the window manager to start accepting input after the device
1159             // is awakened.
1160             SystemClock.sleep(500);
1161         }
1162     }
1163 
1164     /**
1165      * Checks the power manager if the default display is ON.
1166      *
1167      * @return true if the screen is ON else false
1168      * @throws RemoteException
1169      */
isScreenOn()1170     public boolean isScreenOn() throws RemoteException {
1171         return getInteractionController().isScreenOn();
1172     }
1173 
1174     /**
1175      * This method simply presses the power button if the default display is ON, else it does
1176      * nothing if the default display is already OFF.
1177      *
1178      * @throws RemoteException
1179      */
sleep()1180     public void sleep() throws RemoteException {
1181         Log.d(TAG, "Turning off screen.");
1182         getInteractionController().sleepDevice();
1183     }
1184 
1185     /**
1186      * Dumps every window's layout hierarchy to a file in XML format.
1187      *
1188      * @param fileName The file path in which to store the window hierarchy information. Relative
1189      *                file paths are stored the application's internal private storage location.
1190      * @deprecated Use {@link UiDevice#dumpWindowHierarchy(File)} or
1191      *     {@link UiDevice#dumpWindowHierarchy(OutputStream)} instead.
1192      */
1193     @Deprecated
dumpWindowHierarchy(@onNull String fileName)1194     public void dumpWindowHierarchy(@NonNull String fileName) {
1195         File dumpFile = new File(fileName);
1196         if (!dumpFile.isAbsolute()) {
1197             dumpFile = mInstrumentation.getContext().getFileStreamPath(fileName);
1198         }
1199         try {
1200             dumpWindowHierarchy(dumpFile);
1201         } catch (IOException e) {
1202             // Ignore to preserve existing behavior. Ugh.
1203         }
1204     }
1205 
1206     /**
1207      * Dumps every window's layout hierarchy to a {@link java.io.File} in XML format.
1208      *
1209      * @param dest The file in which to store the window hierarchy information.
1210      * @throws IOException if an I/O error occurs
1211      */
dumpWindowHierarchy(@onNull File dest)1212     public void dumpWindowHierarchy(@NonNull File dest) throws IOException {
1213         Log.d(TAG, String.format("Dumping window hierarchy to %s.", dest));
1214         try (OutputStream stream = new BufferedOutputStream(new FileOutputStream(dest))) {
1215             AccessibilityNodeInfoDumper.dumpWindowHierarchy(this, stream);
1216         }
1217     }
1218 
1219     /**
1220      * Dumps every window's layout hierarchy to an {@link java.io.OutputStream} in XML format.
1221      *
1222      * @param out The output stream that the window hierarchy information is written to.
1223      * @throws IOException if an I/O error occurs
1224      */
dumpWindowHierarchy(@onNull OutputStream out)1225     public void dumpWindowHierarchy(@NonNull OutputStream out) throws IOException {
1226         Log.d(TAG, String.format("Dumping window hierarchy to %s.", out));
1227         AccessibilityNodeInfoDumper.dumpWindowHierarchy(this, out);
1228     }
1229 
1230     /**
1231      * Waits for a window content update event to occur.
1232      *
1233      * If a package name for the window is specified, but the current window
1234      * does not have the same package name, the function returns immediately.
1235      *
1236      * @param packageName the specified window package name (can be <code>null</code>).
1237      *        If <code>null</code>, a window update from any front-end window will end the wait
1238      * @param timeout the timeout for the wait
1239      *
1240      * @return true if a window update occurred, false if timeout has elapsed or if the current
1241      *         window does not have the specified package name
1242      */
waitForWindowUpdate(@ullable String packageName, long timeout)1243     public boolean waitForWindowUpdate(@Nullable String packageName, long timeout) {
1244         try (Section ignored = Traces.trace("UiDevice#waitForWindowUpdate")) {
1245             if (packageName != null) {
1246                 if (!packageName.equals(getCurrentPackageName())) {
1247                     Log.w(TAG, String.format("Skipping wait as package %s does not match current "
1248                             + "window %s.", packageName, getCurrentPackageName()));
1249                     return false;
1250                 }
1251             }
1252             Runnable emptyRunnable = () -> {
1253             };
1254             AccessibilityEventFilter checkWindowUpdate = t -> {
1255                 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
1256                     return packageName == null || (t.getPackageName() != null
1257                             && packageName.contentEquals(t.getPackageName()));
1258                 }
1259                 return false;
1260             };
1261             Log.d(TAG, String.format("Waiting %dms for window update of package %s.", timeout,
1262                     packageName));
1263             try {
1264                 getUiAutomation().executeAndWaitForEvent(emptyRunnable, checkWindowUpdate, timeout);
1265             } catch (TimeoutException e) {
1266                 Log.w(TAG, String.format("Timed out waiting %dms on window update.", timeout), e);
1267                 return false;
1268             } catch (Exception e) {
1269                 Log.e(TAG, "Failed to wait for window update.", e);
1270                 return false;
1271             }
1272             return true;
1273         }
1274     }
1275 
1276     /**
1277      * Take a screenshot of current window and store it as PNG
1278      *
1279      * Default scale of 1.0f (original size) and 90% quality is used
1280      * The screenshot is adjusted per screen rotation
1281      *
1282      * @param storePath where the PNG should be written to
1283      * @return true if screen shot is created successfully, false otherwise
1284      */
takeScreenshot(@onNull File storePath)1285     public boolean takeScreenshot(@NonNull File storePath) {
1286         return takeScreenshot(storePath, 1.0f, 90);
1287     }
1288 
1289     /**
1290      * Take a screenshot of current window and store it as PNG
1291      *
1292      * The screenshot is adjusted per screen rotation
1293      *
1294      * @param storePath where the PNG should be written to
1295      * @param scale scale the screenshot down if needed; 1.0f for original size
1296      * @param quality quality of the PNG compression; range: 0-100
1297      * @return true if screen shot is created successfully, false otherwise
1298      */
takeScreenshot(@onNull File storePath, float scale, int quality)1299     public boolean takeScreenshot(@NonNull File storePath, float scale, int quality) {
1300         Log.d(TAG, String.format("Taking screenshot (scale=%f, quality=%d) and storing at %s.",
1301                 scale, quality, storePath));
1302         Bitmap screenshot = getUiAutomation().takeScreenshot();
1303         if (screenshot == null) {
1304             Log.w(TAG, "Failed to take screenshot.");
1305             return false;
1306         }
1307         try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(storePath))) {
1308             screenshot = Bitmap.createScaledBitmap(screenshot,
1309                     Math.round(scale * screenshot.getWidth()),
1310                     Math.round(scale * screenshot.getHeight()), false);
1311             screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos);
1312             bos.flush();
1313             return true;
1314         } catch (IOException ioe) {
1315             Log.e(TAG, "Failed to save screenshot.", ioe);
1316             return false;
1317         } finally {
1318             screenshot.recycle();
1319         }
1320     }
1321 
1322     /**
1323      * Retrieves the default launcher package name.
1324      *
1325      * <p>As of Android 11 (API level 30), apps must declare the packages and intents they intend
1326      * to query. To use this method, an app will need to include the following in its manifest:
1327      * <pre>{@code
1328      * <queries>
1329      *   <intent>
1330      *     <action android:name="android.intent.action.MAIN"/>
1331      *     <category android:name="android.intent.category.HOME"/>
1332      *   </intent>
1333      * </queries>
1334      * }</pre>
1335      *
1336      * @return package name of the default launcher
1337      */
1338     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
getLauncherPackageName()1339     public String getLauncherPackageName() {
1340         Intent intent = new Intent(Intent.ACTION_MAIN);
1341         intent.addCategory(Intent.CATEGORY_HOME);
1342         PackageManager pm = mInstrumentation.getContext().getPackageManager();
1343         ResolveInfo resolveInfo = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
1344         return resolveInfo.activityInfo.packageName;
1345     }
1346 
1347     /**
1348      * Executes a shell command using shell user identity, and return the standard output in string.
1349      * <p>
1350      * Calling function with large amount of output will have memory impacts, and the function call
1351      * will block if the command executed is blocking.
1352      *
1353      * @param cmd the command to run
1354      * @return the standard output of the command
1355      * @throws IOException if an I/O error occurs while reading output
1356      */
1357     @Discouraged(message = "Can be useful for simple commands, but lacks support for proper error"
1358             + " handling, input data, or complex commands (quotes, pipes) that can be obtained "
1359             + "from UiAutomation#executeShellCommandRwe or similar utilities.")
executeShellCommand(@onNull String cmd)1360     public @NonNull String executeShellCommand(@NonNull String cmd) throws IOException {
1361         Log.d(TAG, String.format("Executing shell command: %s", cmd));
1362         try (ParcelFileDescriptor pfd = getUiAutomation().executeShellCommand(cmd);
1363              FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
1364             byte[] buf = new byte[512];
1365             int bytesRead;
1366             StringBuilder stdout = new StringBuilder();
1367             while ((bytesRead = fis.read(buf)) != -1) {
1368                 stdout.append(new String(buf, 0, bytesRead));
1369             }
1370             return stdout.toString();
1371         }
1372     }
1373 
1374     /**
1375      * Gets the display with {@code displayId}. The display may be null because it may be a private
1376      * virtual display, for example.
1377      */
getDisplayById(int displayId)1378     @Nullable Display getDisplayById(int displayId) {
1379         return mDisplayManager.getDisplay(displayId);
1380     }
1381 
1382     /**
1383      * Gets the size of the display with {@code displayId}, in pixels. The size is adjusted based
1384      * on the current orientation of the display.
1385      *
1386      * @see Display#getRealSize(Point)
1387      * @throws IllegalArgumentException when the display with {@code displayId} is not accessible.
1388      */
getDisplaySize(int displayId)1389     Point getDisplaySize(int displayId) {
1390         Point p = new Point();
1391         Display display = getDisplayById(displayId);
1392         if (display == null) {
1393             throw new IllegalArgumentException(String.format("Display %d not found or not "
1394                     + "accessible", displayId));
1395         }
1396         display.getRealSize(p);
1397         return p;
1398     }
1399 
getWindows(UiAutomation uiAutomation)1400     private List<AccessibilityWindowInfo> getWindows(UiAutomation uiAutomation) {
1401         // Support multi-display searches for API level 30 and up.
1402         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1403             final List<AccessibilityWindowInfo> windowList = new ArrayList<>();
1404             final SparseArray<List<AccessibilityWindowInfo>> allWindows =
1405                     Api30Impl.getWindowsOnAllDisplays(uiAutomation);
1406             for (int index = 0; index < allWindows.size(); index++) {
1407                 windowList.addAll(allWindows.valueAt(index));
1408             }
1409             return windowList;
1410         }
1411         return uiAutomation.getWindows();
1412     }
1413 
1414     /**
1415      * Returns a list containing the root {@link AccessibilityNodeInfo}s for each active window.
1416      * For convenience the returned list is sorted in descending window order, ensuring the root of
1417      * the topmost visible window is reported first.
1418      */
1419     @NonNull
getWindowRoots()1420     public List<AccessibilityNodeInfo> getWindowRoots() {
1421         waitForIdle();
1422 
1423         LinkedHashSet<AccessibilityNodeInfo> roots = new LinkedHashSet<>();
1424         UiAutomation uiAutomation = getUiAutomation();
1425 
1426         // Ensure the active window root is included.
1427         AccessibilityNodeInfo activeRoot = uiAutomation.getRootInActiveWindow();
1428         if (activeRoot != null) {
1429             roots.add(activeRoot);
1430         } else {
1431             Log.w(TAG, "Active window root not found.");
1432         }
1433         // Add all windows to support multi-window/display searches.
1434         for (final AccessibilityWindowInfo window : getWindows(uiAutomation)) {
1435             final AccessibilityNodeInfo root = window.getRoot();
1436             if (root == null) {
1437                 Log.w(TAG, "Skipping null root node for window: " + window);
1438                 continue;
1439             }
1440             roots.add(root);
1441         }
1442         return new ArrayList<AccessibilityNodeInfo>(roots);
1443     }
1444 
getInstrumentation()1445     Instrumentation getInstrumentation() {
1446         return mInstrumentation;
1447     }
1448 
getUiContext(int displayId)1449     Context getUiContext(int displayId) {
1450         Context context = mUiContexts.get(displayId);
1451         if (context == null) {
1452             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1453                 final Display display = getDisplayById(displayId);
1454                 if (display != null) {
1455                     context = Api31Impl.createWindowContext(mInstrumentation.getContext(), display);
1456                 } else {
1457                     // The display may be null because it may be private display, for example. In
1458                     // such a case, use the instrumentation's context instead.
1459                     context = mInstrumentation.getContext();
1460                 }
1461             } else {
1462                 context = mInstrumentation.getContext();
1463             }
1464             mUiContexts.put(displayId, context);
1465         }
1466         return context;
1467     }
1468 
getUiAutomation()1469     UiAutomation getUiAutomation() {
1470         UiAutomation uiAutomation;
1471         int flags = Configurator.getInstance().getUiAutomationFlags();
1472         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1473             uiAutomation = Api24Impl.getUiAutomationWithRetry(getInstrumentation(), flags);
1474         } else {
1475             if (flags != Configurator.DEFAULT_UIAUTOMATION_FLAGS) {
1476                 Log.w(TAG, "UiAutomation flags not supported prior to API 24");
1477             }
1478             uiAutomation = getInstrumentation().getUiAutomation();
1479         }
1480 
1481         if (uiAutomation == null) {
1482             throw new NullPointerException("Got null UiAutomation from instrumentation.");
1483         }
1484 
1485         // Verify and update the accessibility service flags if necessary. These might get reset
1486         // if the underlying UiAutomationConnection is recreated.
1487         AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
1488         if (serviceInfo == null) {
1489             Log.w(TAG, "Cannot verify accessibility service flags. "
1490                     + "Multi-window support (searching non-active windows) may be disabled.");
1491             return uiAutomation;
1492         }
1493 
1494         boolean serviceFlagsChanged = serviceInfo.flags != mCachedServiceFlags;
1495         if (serviceFlagsChanged
1496                 || SystemClock.uptimeMillis() - mLastServiceFlagsTime > SERVICE_FLAGS_TIMEOUT) {
1497             // Enable multi-window support.
1498             serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
1499             // Enable or disable hierarchy compression.
1500             if (mCompressed) {
1501                 serviceInfo.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
1502             } else {
1503                 serviceInfo.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
1504             }
1505 
1506             if (serviceFlagsChanged) {
1507                 Log.d(TAG, String.format("Setting accessibility service flags: %d",
1508                         serviceInfo.flags));
1509             }
1510             uiAutomation.setServiceInfo(serviceInfo);
1511             mCachedServiceFlags = serviceInfo.flags;
1512             mLastServiceFlagsTime = SystemClock.uptimeMillis();
1513         }
1514 
1515         return uiAutomation;
1516     }
1517 
getQueryController()1518     QueryController getQueryController() {
1519         return mQueryController;
1520     }
1521 
getInteractionController()1522     InteractionController getInteractionController() {
1523         return mInteractionController;
1524     }
1525 
1526     @RequiresApi(24)
1527     static class Api24Impl {
Api24Impl()1528         private Api24Impl() {
1529         }
1530 
getUiAutomationWithRetry(Instrumentation instrumentation, int flags)1531         static UiAutomation getUiAutomationWithRetry(Instrumentation instrumentation, int flags) {
1532             UiAutomation uiAutomation = null;
1533             for (int i = 0; i < MAX_UIAUTOMATION_RETRY; i++) {
1534                 uiAutomation = instrumentation.getUiAutomation(flags);
1535                 if (uiAutomation != null) {
1536                     break;
1537                 }
1538                 if (i < MAX_UIAUTOMATION_RETRY - 1) {
1539                     Log.e(TAG, "Got null UiAutomation from instrumentation - Retrying...");
1540                     SystemClock.sleep(UIAUTOMATION_RETRY_INTERVAL);
1541                 }
1542             }
1543             return uiAutomation;
1544         }
1545     }
1546 
1547     @RequiresApi(30)
1548     static class Api30Impl {
Api30Impl()1549         private Api30Impl() {
1550         }
1551 
getWindowsOnAllDisplays( UiAutomation uiAutomation)1552         static SparseArray<List<AccessibilityWindowInfo>> getWindowsOnAllDisplays(
1553                 UiAutomation uiAutomation) {
1554             return uiAutomation.getWindowsOnAllDisplays();
1555         }
1556     }
1557 
1558     @RequiresApi(31)
1559     static class Api31Impl {
Api31Impl()1560         private Api31Impl() {
1561         }
1562 
createWindowContext(Context context, Display display)1563         static Context createWindowContext(Context context, Display display) {
1564             return context.createWindowContext(display,
1565                     WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, null);
1566         }
1567     }
1568 }
1569