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