UiDevice.java revision 9fe7f8d9e5f83bd13bc2b06c672d3cd93f619be0
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 17package com.android.uiautomator.core; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.Canvas; 22import android.graphics.Matrix; 23import android.graphics.Point; 24import android.hardware.display.DisplayManagerGlobal; 25import android.os.Build; 26import android.os.Environment; 27import android.os.RemoteException; 28import android.os.ServiceManager; 29import android.os.SystemClock; 30import android.util.DisplayMetrics; 31import android.util.Log; 32import android.view.Display; 33import android.view.KeyEvent; 34import android.view.Surface; 35import android.view.accessibility.AccessibilityEvent; 36import android.view.accessibility.AccessibilityNodeInfo; 37 38import com.android.internal.statusbar.IStatusBarService; 39import com.android.internal.util.Predicate; 40 41import java.io.File; 42import java.io.FileOutputStream; 43import java.io.IOException; 44import java.util.ArrayList; 45import java.util.HashMap; 46import java.util.List; 47import java.util.concurrent.TimeoutException; 48 49/** 50 * UiDevice provides access to device wide states. Also provides methods to simulate 51 * pressing hardware buttons such as DPad or the soft buttons such as Home and Menu. 52 */ 53public class UiDevice { 54 private static final String LOG_TAG = UiDevice.class.getSimpleName(); 55 56 private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000; 57 58 // store for registered UiWatchers 59 private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); 60 private final List<String> mWatchersTriggers = new ArrayList<String>(); 61 62 // remember if we're executing in the context of a UiWatcher 63 private boolean mInWatcherContext = false; 64 65 // provides access the {@link QueryController} and {@link InteractionController} 66 private final UiAutomatorBridge mUiAutomationBridge; 67 68 // reference to self 69 private static UiDevice mDevice; 70 71 private UiDevice() { 72 mUiAutomationBridge = new UiAutomatorBridge(); 73 mDevice = this; 74 } 75 76 boolean isInWatcherContext() { 77 return mInWatcherContext; 78 } 79 80 /** 81 * Provides access the {@link QueryController} and {@link InteractionController} 82 * @return {@link UiAutomatorBridge} 83 */ 84 UiAutomatorBridge getAutomatorBridge() { 85 return mUiAutomationBridge; 86 } 87 /** 88 * Allow both the direct creation of a UiDevice and retrieving a existing 89 * instance of UiDevice. This helps tests and their libraries to have access 90 * to UiDevice with necessitating having to always pass copies of UiDevice 91 * instances around. 92 * @return UiDevice instance 93 */ 94 public static UiDevice getInstance() { 95 if (mDevice == null) { 96 mDevice = new UiDevice(); 97 } 98 return mDevice; 99 } 100 101 /** 102 * Returns the display size in dp (device-independent pixel) 103 * 104 * The returned display size is adjusted per screen rotation 105 * 106 * @return 107 * @hide 108 */ 109 public Point getDisplaySizeDp() { 110 Display display = getDefaultDisplay(); 111 Point p = new Point(); 112 display.getSize(p); 113 DisplayMetrics metrics = new DisplayMetrics(); 114 display.getMetrics(metrics); 115 float dpx = p.x / metrics.density; 116 float dpy = p.y / metrics.density; 117 p.x = Math.round(dpx); 118 p.y = Math.round(dpy); 119 return p; 120 } 121 122 /** 123 * Returns the product name of the device 124 * 125 * This provides info on what type of device that the test is running on. However, for the 126 * purpose of adapting to different styles of UI, test should favor 127 * {@link UiDevice#getDisplaySizeDp()} over this method, and only use product name as a fallback 128 * mechanism 129 */ 130 public String getProductName() { 131 return Build.PRODUCT; 132 } 133 134 /** 135 * This method returns the text from the last UI traversal event received. 136 * This is helpful in WebView when the test performs directional arrow presses to focus 137 * on different elements inside the WebView. The accessibility fires events 138 * with every text highlighted. One can read the contents of a WebView control this way 139 * however slow slow and unreliable it is. When the view control used can return a 140 * reference to is Document Object Model, it is recommended then to use the view's 141 * DOM instead. 142 * @return text of the last traversal event else an empty string 143 */ 144 public String getLastTraversedText() { 145 return mUiAutomationBridge.getQueryController().getLastTraversedText(); 146 } 147 148 /** 149 * Helper to clear the text saved from the last accessibility UI traversal event. 150 * See {@link #getLastTraversedText()}. 151 */ 152 public void clearLastTraversedText() { 153 mUiAutomationBridge.getQueryController().clearLastTraversedText(); 154 } 155 156 /** 157 * Helper method to do a short press on MENU button 158 * @return true if successful else false 159 */ 160 public boolean pressMenu() { 161 return pressKeyCode(KeyEvent.KEYCODE_MENU); 162 } 163 164 /** 165 * Helper method to do a short press on BACK button 166 * @return true if successful else false 167 */ 168 public boolean pressBack() { 169 return pressKeyCode(KeyEvent.KEYCODE_BACK); 170 } 171 172 /** 173 * Helper method to do a short press on HOME button 174 * @return true if successful else false 175 */ 176 public boolean pressHome() { 177 return pressKeyCode(KeyEvent.KEYCODE_HOME); 178 } 179 180 /** 181 * Helper method to do a short press on SEARCH button 182 * @return true if successful else false 183 */ 184 public boolean pressSearch() { 185 return pressKeyCode(KeyEvent.KEYCODE_SEARCH); 186 } 187 188 /** 189 * Helper method to do a short press on DOWN button 190 * @return true if successful else false 191 */ 192 public boolean pressDPadCenter() { 193 return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); 194 } 195 196 /** 197 * Helper method to do a short press on DOWN button 198 * @return true if successful else false 199 */ 200 public boolean pressDPadDown() { 201 return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); 202 } 203 204 /** 205 * Helper method to do a short press on UP button 206 * @return true if successful else false 207 */ 208 public boolean pressDPadUp() { 209 return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); 210 } 211 212 /** 213 * Helper method to do a short press on LEFT button 214 * @return true if successful else false 215 */ 216 public boolean pressDPadLeft() { 217 return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); 218 } 219 220 /** 221 * Helper method to do a short press on RIGTH button 222 * @return true if successful else false 223 */ 224 public boolean pressDPadRight() { 225 return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); 226 } 227 228 /** 229 * Helper method to do a short press on DELETE 230 * @return true if successful else false 231 */ 232 public boolean pressDelete() { 233 return pressKeyCode(KeyEvent.KEYCODE_DEL); 234 } 235 236 /** 237 * Helper method to do a short press on ENTER 238 * @return true if successful else false 239 */ 240 public boolean pressEnter() { 241 return pressKeyCode(KeyEvent.KEYCODE_ENTER); 242 } 243 244 /** 245 * Helper method to do a short press using a key code. See {@link KeyEvent} 246 * @return true if successful else false 247 */ 248 public boolean pressKeyCode(int keyCode) { 249 waitForIdle(); 250 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0); 251 } 252 253 /** 254 * Helper method to do a short press using a key code. See {@link KeyEvent} 255 * @param keyCode See {@link KeyEvent} 256 * @param metaState See {@link KeyEvent} 257 * @return true if successful else false 258 */ 259 public boolean pressKeyCode(int keyCode, int metaState) { 260 waitForIdle(); 261 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState); 262 } 263 264 /** 265 * Press recent apps soft key 266 * @return true if successful 267 * @throws RemoteException 268 */ 269 public boolean pressRecentApps() throws RemoteException { 270 waitForIdle(); 271 final IStatusBarService statusBar = IStatusBarService.Stub.asInterface( 272 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 273 274 if (statusBar != null) { 275 statusBar.toggleRecentApps(); 276 return true; 277 } 278 return false; 279 } 280 281 /** 282 * Gets the width of the display, in pixels. The width and height details 283 * are reported based on the current orientation of the display. 284 * @return width in pixels or zero on failure 285 */ 286 public int getDisplayWidth() { 287 Display display = getDefaultDisplay(); 288 Point p = new Point(); 289 display.getSize(p); 290 return p.x; 291 } 292 293 /** 294 * Gets the height of the display, in pixels. The size is adjusted based 295 * on the current orientation of the display. 296 * @return height in pixels or zero on failure 297 */ 298 public int getDisplayHeight() { 299 Display display = getDefaultDisplay(); 300 Point p = new Point(); 301 display.getSize(p); 302 return p.y; 303 } 304 305 /** 306 * Perform a click at arbitrary coordinates specified by the user 307 * 308 * @param x coordinate 309 * @param y coordinate 310 * @return true if the click succeeded else false 311 */ 312 public boolean click(int x, int y) { 313 if (x >= getDisplayWidth() || y >= getDisplayHeight()) { 314 return (false); 315 } 316 return getAutomatorBridge().getInteractionController().click(x, y); 317 } 318 319 /** 320 * Performs a swipe from one coordinate to another using the number of steps 321 * to determine smoothness and speed. Each step execution is throttled to 5ms 322 * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. 323 * 324 * @param startX 325 * @param startY 326 * @param endX 327 * @param endY 328 * @param steps is the number of move steps sent to the system 329 * @return false if the operation fails or the coordinates are invalid 330 */ 331 public boolean swipe(int startX, int startY, int endX, int endY, int steps) { 332 return mUiAutomationBridge.getInteractionController() 333 .scrollSwipe(startX, startY, endX, endY, steps); 334 } 335 336 /** 337 * Performs a swipe between points in the Point array. Each step execution is throttled 338 * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete 339 * 340 * @param segments is Point array containing at least one Point object 341 * @param segmentSteps steps to inject between two Points 342 * @return true on success 343 */ 344 public boolean swipe(Point[] segments, int segmentSteps) { 345 return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); 346 } 347 348 public void waitForIdle() { 349 waitForIdle(DEFAULT_TIMEOUT_MILLIS); 350 } 351 352 public void waitForIdle(long time) { 353 mUiAutomationBridge.waitForIdle(time); 354 } 355 356 /** 357 * Last activity to report accessibility events 358 * @return String name of activity 359 */ 360 public String getCurrentActivityName() { 361 return mUiAutomationBridge.getQueryController().getCurrentActivityName(); 362 } 363 364 /** 365 * Last package to report accessibility events 366 * @return String name of package 367 */ 368 public String getCurrentPackageName() { 369 return mUiAutomationBridge.getQueryController().getCurrentPackageName(); 370 } 371 372 /** 373 * Registers a condition watcher to be called by the automation library only when a 374 * {@link UiObject} method call is in progress and is in retry waiting to match 375 * its UI element. Only during these conditions the watchers are invoked to check if 376 * there is something else unexpected on the screen that may be causing the match failure 377 * and retries. Under normal conditions when UiObject methods are immediately matching 378 * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()} 379 * 380 * @param name of watcher 381 * @param watcher {@link UiWatcher} 382 */ 383 public void registerWatcher(String name, UiWatcher watcher) { 384 if (mInWatcherContext) { 385 throw new IllegalStateException("Cannot register new watcher from within another"); 386 } 387 mWatchers.put(name, watcher); 388 } 389 390 /** 391 * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}. 392 * 393 * @param name of watcher used when <code>registerWatcher</code> was called. 394 * @throws UiAutomationException 395 */ 396 public void removeWatcher(String name) { 397 if (mInWatcherContext) { 398 throw new IllegalStateException("Cannot remove a watcher from within another"); 399 } 400 mWatchers.remove(name); 401 } 402 403 /** 404 * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers 405 * to run. 406 */ 407 public void runWatchers() { 408 if (mInWatcherContext) { 409 return; 410 } 411 412 for (String watcherName : mWatchers.keySet()) { 413 UiWatcher watcher = mWatchers.get(watcherName); 414 if (watcher != null) { 415 try { 416 mInWatcherContext = true; 417 if (watcher.checkForCondition()) { 418 setWatcherTriggered(watcherName); 419 } 420 } catch (Exception e) { 421 Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); 422 } finally { 423 mInWatcherContext = false; 424 } 425 } 426 } 427 } 428 429 /** 430 * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and 431 * returns true from its implementation of {@link UiWatcher#checkForCondition()} then 432 * it is considered triggered. 433 */ 434 public void resetWatcherTriggers() { 435 mWatchersTriggers.clear(); 436 } 437 438 /** 439 * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and 440 * returns true from its implementation of {@link UiWatcher#checkForCondition()} then 441 * it is considered triggered. This method can be used to check if a specific UiWatcher 442 * has been triggered during the test. This is helpful if a watcher is detecting errors 443 * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. 444 */ 445 public boolean hasWatcherTriggered(String watcherName) { 446 return mWatchersTriggers.contains(watcherName); 447 } 448 449 /** 450 * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)} 451 */ 452 public boolean hasAnyWatcherTriggered() { 453 return mWatchersTriggers.size() > 0; 454 } 455 456 private void setWatcherTriggered(String watcherName) { 457 if (!hasWatcherTriggered(watcherName)) { 458 mWatchersTriggers.add(watcherName); 459 } 460 } 461 462 /** 463 * Check if the device is in its natural orientation. This is determined by checking if the 464 * orientation is at 0 or 180 degrees. 465 * @return true if it is in natural orientation 466 */ 467 public boolean isNaturalOrientation() { 468 Display display = getDefaultDisplay(); 469 return display.getRotation() == Surface.ROTATION_0 || 470 display.getRotation() == Surface.ROTATION_180; 471 } 472 473 /** 474 * Returns the current rotation of the display, as defined in {@link Surface} 475 * @return 476 */ 477 public int getDisplayRotation() { 478 return getDefaultDisplay().getRotation(); 479 } 480 481 /** 482 * Disables the sensors and freezes the device rotation at its 483 * current rotation state. 484 * @throws RemoteException 485 */ 486 public void freezeRotation() throws RemoteException { 487 getAutomatorBridge().getInteractionController().freezeRotation(); 488 } 489 490 /** 491 * Re-enables the sensors and un-freezes the device rotation allowing its contents 492 * to rotate with the device physical rotation. Note that by un-freezing the rotation, 493 * the screen contents may suddenly rotate depending on the current physical position 494 * of the test device. During a test execution, it is best to keep the device frozen 495 * in a specific orientation until the test case execution is completed. 496 * @throws RemoteException 497 */ 498 public void unfreezeRotation() throws RemoteException { 499 getAutomatorBridge().getInteractionController().unfreezeRotation(); 500 } 501 502 /** 503 * Orients the device to the left and also freezes rotation in that 504 * orientation by disabling the sensors. If you want to un-freeze the rotation 505 * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing 506 * so may cause the screen contents to get re-oriented depending on the current 507 * physical position of the test device. 508 * @throws RemoteException 509 */ 510 public void setOrientationLeft() throws RemoteException { 511 getAutomatorBridge().getInteractionController().setRotationLeft(); 512 } 513 514 /** 515 * Orients the device to the right and also freezes rotation in that 516 * orientation by disabling the sensors. If you want to un-freeze the rotation 517 * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing 518 * so may cause the screen contents to get re-oriented depending on the current 519 * physical position of the test device. 520 * @throws RemoteException 521 */ 522 public void setOrientationRight() throws RemoteException { 523 getAutomatorBridge().getInteractionController().setRotationRight(); 524 } 525 526 /** 527 * Rotates right and also freezes rotation in that orientation by 528 * disabling the sensors. If you want to un-freeze the rotation 529 * and re-enable the sensors see {@link #unfreezeRotation()}. Note 530 * that doing so may cause the screen contents to rotate 531 * depending on the current physical position of the test device. 532 * @throws RemoteException 533 */ 534 public void setOrientationNatural() throws RemoteException { 535 getAutomatorBridge().getInteractionController().setRotationNatural(); 536 } 537 538 /** 539 * This method simply presses the power button if the screen is OFF else 540 * it does nothing if the screen is already ON. If the screen was OFF and 541 * it just got turned ON, this method will insert a 500ms delay to allow 542 * the device time to wake up and accept input. 543 * @throws RemoteException 544 */ 545 public void wakeUp() throws RemoteException { 546 if(getAutomatorBridge().getInteractionController().wakeDevice()) { 547 // sync delay to allow the window manager to start accepting input 548 // after the device is awakened. 549 SystemClock.sleep(500); 550 } 551 } 552 553 /** 554 * Checks the power manager if the screen is ON 555 * @return true if the screen is ON else false 556 * @throws RemoteException 557 */ 558 public boolean isScreenOn() throws RemoteException { 559 return getAutomatorBridge().getInteractionController().isScreenOn(); 560 } 561 562 /** 563 * This method simply presses the power button if the screen is ON else 564 * it does nothing if the screen is already OFF. 565 * @throws RemoteException 566 */ 567 public void sleep() throws RemoteException { 568 getAutomatorBridge().getInteractionController().sleepDevice(); 569 } 570 571 /** 572 * Helper method used for debugging to dump the current window's layout hierarchy. 573 * The file root location is /data/local/tmp 574 * 575 * @param fileName 576 */ 577 public void dumpWindowHierarchy(String fileName) { 578 AccessibilityNodeInfo root = 579 getAutomatorBridge().getQueryController().getAccessibilityRootNode(); 580 if(root != null) { 581 AccessibilityNodeInfoDumper.dumpWindowToFile( 582 root, new File(new File(Environment.getDataDirectory(), 583 "local/tmp"), fileName)); 584 } 585 } 586 587 /** 588 * Waits for a window content update event to occur 589 * 590 * if a package name for window is specified, but current window is not with the same package 591 * name, the function will return immediately 592 * 593 * @param packageName the specified window package name; maybe <code>null</code>, and a window 594 * update from any frontend window will end the wait 595 * @param timeout the timeout for the wait 596 * 597 * @return true if a window update occured, false if timeout has reached or current window is 598 * not the specified package name 599 */ 600 public boolean waitForWindowUpdate(final String packageName, long timeout) { 601 if (packageName != null) { 602 if (!packageName.equals(getCurrentPackageName())) { 603 return false; 604 } 605 } 606 Runnable emptyRunnable = new Runnable() { 607 @Override 608 public void run() { 609 } 610 }; 611 Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { 612 @Override 613 public boolean apply(AccessibilityEvent t) { 614 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { 615 return packageName == null || packageName.equals(t.getPackageName()); 616 } 617 return false; 618 } 619 }; 620 try { 621 getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( 622 emptyRunnable, checkWindowUpdate, timeout); 623 } catch (TimeoutException e) { 624 return false; 625 } catch (Exception e) { 626 Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); 627 return false; 628 } 629 return true; 630 } 631 632 private static Display getDefaultDisplay() { 633 return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY); 634 } 635 636 /** 637 * @return the current display rotation in degrees 638 */ 639 private static float getDegreesForRotation(int value) { 640 switch (value) { 641 case Surface.ROTATION_90: 642 return 360f - 90f; 643 case Surface.ROTATION_180: 644 return 360f - 180f; 645 case Surface.ROTATION_270: 646 return 360f - 270f; 647 } 648 return 0f; 649 } 650 651 /** 652 * Take a screenshot of current window and store it as PNG 653 * 654 * Default scale of 1.0f (original size) and 90% quality is used 655 * 656 * @param storePath where the PNG should be written to 657 * @return 658 */ 659 public boolean takeScreenshot(File storePath) { 660 return takeScreenshot(storePath, 1.0f, 90); 661 } 662 663 /** 664 * Take a screenshot of current window and store it as PNG 665 * 666 * The screenshot is adjusted per screen rotation; 667 * 668 * @param storePath where the PNG should be written to 669 * @param scale scale the screenshot down if needed; 1.0f for original size 670 * @param quality quality of the PNG compression; range: 0-100 671 * @return 672 */ 673 public boolean takeScreenshot(File storePath, float scale, int quality) { 674 // This is from com.android.systemui.screenshot.GlobalScreenshot#takeScreenshot 675 // We need to orient the screenshot correctly (and the Surface api seems to take screenshots 676 // only in the natural orientation of the device :!) 677 DisplayMetrics displayMetrics = new DisplayMetrics(); 678 Display display = getDefaultDisplay(); 679 display.getRealMetrics(displayMetrics); 680 float[] dims = {displayMetrics.widthPixels, displayMetrics.heightPixels}; 681 float degrees = getDegreesForRotation(display.getRotation()); 682 boolean requiresRotation = (degrees > 0); 683 Matrix matrix = new Matrix(); 684 matrix.reset(); 685 if (scale != 1.0f) { 686 matrix.setScale(scale, scale); 687 } 688 if (requiresRotation) { 689 // Get the dimensions of the device in its native orientation 690 matrix.preRotate(-degrees); 691 } 692 matrix.mapPoints(dims); 693 dims[0] = Math.abs(dims[0]); 694 dims[1] = Math.abs(dims[1]); 695 696 // Take the screenshot 697 Bitmap screenShot = Surface.screenshot((int) dims[0], (int) dims[1]); 698 if (screenShot == null) { 699 return false; 700 } 701 702 if (requiresRotation) { 703 // Rotate the screenshot to the current orientation 704 int width = displayMetrics.widthPixels; 705 int height = displayMetrics.heightPixels; 706 if (scale != 1.0f) { 707 width = Math.round(scale * width); 708 height = Math.round(scale * height); 709 } 710 Bitmap ss = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 711 Canvas c = new Canvas(ss); 712 c.translate(ss.getWidth() / 2, ss.getHeight() / 2); 713 c.rotate(degrees); 714 c.translate(-dims[0] / 2, -dims[1] / 2); 715 c.drawBitmap(screenShot, 0, 0, null); 716 c.setBitmap(null); 717 screenShot = ss; 718 } 719 720 // Optimizations 721 screenShot.setHasAlpha(false); 722 723 try { 724 FileOutputStream fos = new FileOutputStream(storePath); 725 screenShot.compress(Bitmap.CompressFormat.PNG, quality, fos); 726 fos.flush(); 727 fos.close(); 728 } catch (IOException ioe) { 729 Log.e(LOG_TAG, "failed to save screen shot to file", ioe); 730 return false; 731 } finally { 732 screenShot.recycle(); 733 } 734 return true; 735 } 736} 737