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