UiDevice.java revision 467cca7d25dbbf13c14cfd8c2ad38ab8eaf56bda
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.os.Build; 22import android.os.Environment; 23import android.os.RemoteException; 24import android.os.ServiceManager; 25import android.os.SystemClock; 26import android.util.DisplayMetrics; 27import android.util.Log; 28import android.view.Display; 29import android.view.IWindowManager; 30import android.view.KeyEvent; 31import android.view.Surface; 32import android.view.WindowManagerImpl; 33import android.view.accessibility.AccessibilityEvent; 34import android.view.accessibility.AccessibilityNodeInfo; 35 36import com.android.internal.statusbar.IStatusBarService; 37import com.android.internal.util.Predicate; 38 39import java.io.File; 40import java.util.ArrayList; 41import java.util.HashMap; 42import java.util.List; 43import java.util.concurrent.TimeoutException; 44 45/** 46 * UiDevice provides access to device wide states. Also provides methods to simulate 47 * pressing hardware buttons such as DPad or the soft buttons such as Home and Menu. 48 */ 49public class UiDevice { 50 private static final String LOG_TAG = UiDevice.class.getSimpleName(); 51 52 private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000; 53 54 // store for registered UiWatchers 55 private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); 56 private final List<String> mWatchersTriggers = new ArrayList<String>(); 57 58 // remember if we're executing in the context of a UiWatcher 59 private boolean mInWatcherContext = false; 60 61 // provides access the {@link QueryController} and {@link InteractionController} 62 private final UiAutomatorBridge mUiAutomationBridge; 63 64 // reference to self 65 private static UiDevice mDevice; 66 67 private UiDevice() { 68 mUiAutomationBridge = new UiAutomatorBridge(); 69 mDevice = this; 70 } 71 72 boolean isInWatcherContext() { 73 return mInWatcherContext; 74 } 75 76 /** 77 * Provides access the {@link QueryController} and {@link InteractionController} 78 * @return {@link UiAutomatorBridge} 79 */ 80 UiAutomatorBridge getAutomatorBridge() { 81 return mUiAutomationBridge; 82 } 83 /** 84 * Allow both the direct creation of a UiDevice and retrieving a existing 85 * instance of UiDevice. This helps tests and their libraries to have access 86 * to UiDevice with necessitating having to always pass copies of UiDevice 87 * instances around. 88 * @return UiDevice instance 89 */ 90 public static UiDevice getInstance() { 91 if (mDevice == null) { 92 mDevice = new UiDevice(); 93 } 94 return mDevice; 95 } 96 97 /** 98 * Returns the display size in dp (device-independent pixel) 99 * 100 * The returned display size is adjusted per screen rotation 101 * 102 * @return 103 */ 104 public Point getDisplaySizeDp() { 105 Display display = WindowManagerImpl.getDefault().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 * Gets the width of the display, in pixels. The width and height details 261 * are reported based on the current orientation of the display. 262 * @return width in pixels or zero on failure 263 */ 264 public int getDisplayWidth() { 265 IWindowManager wm = IWindowManager.Stub.asInterface( 266 ServiceManager.getService(Context.WINDOW_SERVICE)); 267 Point p = new Point(); 268 try { 269 wm.getDisplaySize(p); 270 } catch (RemoteException e) { 271 return 0; 272 } 273 return p.x; 274 } 275 276 /** 277 * Press recent apps soft key 278 * @return true if successful 279 * @throws RemoteException 280 */ 281 public boolean pressRecentApps() throws RemoteException { 282 waitForIdle(); 283 final IStatusBarService statusBar = IStatusBarService.Stub.asInterface( 284 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 285 286 if (statusBar != null) { 287 statusBar.toggleRecentApps(); 288 return true; 289 } 290 return false; 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 IWindowManager wm = IWindowManager.Stub.asInterface( 300 ServiceManager.getService(Context.WINDOW_SERVICE)); 301 Point p = new Point(); 302 try { 303 wm.getDisplaySize(p); 304 } catch (RemoteException e) { 305 return 0; 306 } 307 return p.y; 308 } 309 310 /** 311 * Perform a click at arbitrary coordinates specified by the user 312 * 313 * @param x coordinate 314 * @param y coordinate 315 * @return true if the click succeeded else false 316 */ 317 public boolean click(int x, int y) { 318 if (x >= getDisplayWidth() || y >= getDisplayHeight()) { 319 return (false); 320 } 321 return getAutomatorBridge().getInteractionController().click(x, y); 322 } 323 324 /** 325 * Performs a swipe from one coordinate to another using the number of steps 326 * to determine smoothness and speed. Each step execution is throttled to 5ms 327 * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. 328 * 329 * @param startX 330 * @param startY 331 * @param endX 332 * @param endY 333 * @param steps is the number of move steps sent to the system 334 * @return false if the operation fails or the coordinates are invalid 335 */ 336 public boolean swipe(int startX, int startY, int endX, int endY, int steps) { 337 return mUiAutomationBridge.getInteractionController() 338 .scrollSwipe(startX, startY, endX, endY, steps); 339 } 340 341 /** 342 * Performs a swipe between points in the Point array. Each step execution is throttled 343 * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete 344 * 345 * @param segments is Point array containing at least one Point object 346 * @param segmentSteps steps to inject between two Points 347 * @return true on success 348 */ 349 public boolean swipe(Point[] segments, int segmentSteps) { 350 return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); 351 } 352 353 public void waitForIdle() { 354 waitForIdle(DEFAULT_TIMEOUT_MILLIS); 355 } 356 357 public void waitForIdle(long time) { 358 mUiAutomationBridge.waitForIdle(time); 359 } 360 361 /** 362 * Last activity to report accessibility events 363 * @return String name of activity 364 */ 365 public String getCurrentActivityName() { 366 return mUiAutomationBridge.getQueryController().getCurrentActivityName(); 367 } 368 369 /** 370 * Last package to report accessibility events 371 * @return String name of package 372 */ 373 public String getCurrentPackageName() { 374 return mUiAutomationBridge.getQueryController().getCurrentPackageName(); 375 } 376 377 /** 378 * Registers a condition watcher to be called by the automation library only when a 379 * {@link UiObject} method call is in progress and is in retry waiting to match 380 * its UI element. Only during these conditions the watchers are invoked to check if 381 * there is something else unexpected on the screen that may be causing the match failure 382 * and retries. Under normal conditions when UiObject methods are immediately matching 383 * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()} 384 * 385 * @param name of watcher 386 * @param watcher {@link UiWatcher} 387 */ 388 public void registerWatcher(String name, UiWatcher watcher) { 389 if (mInWatcherContext) { 390 throw new IllegalStateException("Cannot register new watcher from within another"); 391 } 392 mWatchers.put(name, watcher); 393 } 394 395 /** 396 * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}. 397 * 398 * @param name of watcher used when <code>registerWatcher</code> was called. 399 * @throws UiAutomationException 400 */ 401 public void removeWatcher(String name) { 402 if (mInWatcherContext) { 403 throw new IllegalStateException("Cannot remove a watcher from within another"); 404 } 405 mWatchers.remove(name); 406 } 407 408 /** 409 * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers 410 * to run. 411 */ 412 public void runWatchers() { 413 if (mInWatcherContext) { 414 return; 415 } 416 417 for (String watcherName : mWatchers.keySet()) { 418 UiWatcher watcher = mWatchers.get(watcherName); 419 if (watcher != null) { 420 try { 421 mInWatcherContext = true; 422 if (watcher.checkForCondition()) { 423 setWatcherTriggered(watcherName); 424 } 425 } catch (Exception e) { 426 Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); 427 } finally { 428 mInWatcherContext = false; 429 } 430 } 431 } 432 } 433 434 /** 435 * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and 436 * returns true from its implementation of {@link UiWatcher#checkForCondition()} then 437 * it is considered triggered. 438 */ 439 public void resetWatcherTriggers() { 440 mWatchersTriggers.clear(); 441 } 442 443 /** 444 * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and 445 * returns true from its implementation of {@link UiWatcher#checkForCondition()} then 446 * it is considered triggered. This method can be used to check if a specific UiWatcher 447 * has been triggered during the test. This is helpful if a watcher is detecting errors 448 * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. 449 */ 450 public boolean hasWatcherTriggered(String watcherName) { 451 return mWatchersTriggers.contains(watcherName); 452 } 453 454 /** 455 * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)} 456 */ 457 public boolean hasAnyWatcherTriggered() { 458 return mWatchersTriggers.size() > 0; 459 } 460 461 private void setWatcherTriggered(String watcherName) { 462 if (!hasWatcherTriggered(watcherName)) { 463 mWatchersTriggers.add(watcherName); 464 } 465 } 466 467 /** 468 * Check if the device is in its natural orientation. This is determined by checking if the 469 * orientation is at 0 or 180 degrees. 470 * @return true if it is in natural orientation 471 */ 472 public boolean isNaturalOrientation() { 473 Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); 474 return display.getRotation() == Surface.ROTATION_0 || 475 display.getRotation() == Surface.ROTATION_180; 476 } 477 478 /** 479 * Disables the sensors and freezes the device rotation at its 480 * current rotation state. 481 * @throws RemoteException 482 */ 483 public void freezeRotation() throws RemoteException { 484 getAutomatorBridge().getInteractionController().freezeRotation(); 485 } 486 487 /** 488 * Re-enables the sensors and un-freezes the device rotation allowing its contents 489 * to rotate with the device physical rotation. Note that by un-freezing the rotation, 490 * the screen contents may suddenly rotate depending on the current physical position 491 * of the test device. During a test execution, it is best to keep the device frozen 492 * in a specific orientation until the test case execution is completed. 493 * @throws RemoteException 494 */ 495 public void unfreezeRotation() throws RemoteException { 496 getAutomatorBridge().getInteractionController().unfreezeRotation(); 497 } 498 499 /** 500 * Orients the device to the left and also freezes rotation in that 501 * orientation by disabling the sensors. If you want to un-freeze the rotation 502 * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing 503 * so may cause the screen contents to get re-oriented depending on the current 504 * physical position of the test device. 505 * @throws RemoteException 506 */ 507 public void setOrientationLeft() throws RemoteException { 508 getAutomatorBridge().getInteractionController().setRotationLeft(); 509 } 510 511 /** 512 * Orients the device to the right and also freezes rotation in that 513 * orientation by disabling the sensors. If you want to un-freeze the rotation 514 * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing 515 * so may cause the screen contents to get re-oriented depending on the current 516 * physical position of the test device. 517 * @throws RemoteException 518 */ 519 public void setOrientationRight() throws RemoteException { 520 getAutomatorBridge().getInteractionController().setRotationRight(); 521 } 522 523 /** 524 * Rotates right and also freezes rotation in that orientation by 525 * disabling the sensors. If you want to un-freeze the rotation 526 * and re-enable the sensors see {@link #unfreezeRotation()}. Note 527 * that doing so may cause the screen contents to rotate 528 * depending on the current physical position of the test device. 529 * @throws RemoteException 530 */ 531 public void setOrientationNatural() throws RemoteException { 532 getAutomatorBridge().getInteractionController().setRotationNatural(); 533 } 534 535 /** 536 * This method simply presses the power button if the screen is OFF else 537 * it does nothing if the screen is already ON. If the screen was OFF and 538 * it just got turned ON, this method will insert a 500ms delay to allow 539 * the device time to wake up and accept input. 540 * @throws RemoteException 541 */ 542 public void wakeUp() throws RemoteException { 543 if(getAutomatorBridge().getInteractionController().wakeDevice()) { 544 // sync delay to allow the window manager to start accepting input 545 // after the device is awakened. 546 SystemClock.sleep(500); 547 } 548 } 549 550 /** 551 * Checks the power manager if the screen is ON 552 * @return true if the screen is ON else false 553 * @throws RemoteException 554 */ 555 public boolean isScreenOn() throws RemoteException { 556 return getAutomatorBridge().getInteractionController().isScreenOn(); 557 } 558 559 /** 560 * This method simply presses the power button if the screen is ON else 561 * it does nothing if the screen is already OFF. 562 * @throws RemoteException 563 */ 564 public void sleep() throws RemoteException { 565 getAutomatorBridge().getInteractionController().sleepDevice(); 566 } 567 568 /** 569 * Helper method used for debugging to dump the current window's layout hierarchy. 570 * The file root location is /data/local/tmp 571 * 572 * @param fileName 573 */ 574 public void dumpWindowHierarchy(String fileName) { 575 AccessibilityNodeInfo root = 576 getAutomatorBridge().getQueryController().getAccessibilityRootNode(); 577 if(root != null) { 578 AccessibilityNodeInfoDumper.dumpWindowToFile( 579 root, new File(new File(Environment.getDataDirectory(), 580 "local/tmp"), fileName)); 581 } 582 } 583 584 /** 585 * Waits for a window content update event to occur 586 * 587 * if a package name for window is specified, but current window is not with the same package 588 * name, the function will return immediately 589 * 590 * @param packageName the specified window package name; maybe <code>null</code>, and a window 591 * update from any frontend window will end the wait 592 * @param timeout the timeout for the wait 593 * 594 * @return true if a window update occured, false if timeout has reached or current window is 595 * not the specified package name 596 */ 597 public boolean waitForWindowUpdate(final String packageName, long timeout) { 598 if (packageName != null) { 599 if (!packageName.equals(getCurrentPackageName())) { 600 return false; 601 } 602 } 603 Runnable emptyRunnable = new Runnable() { 604 @Override 605 public void run() { 606 } 607 }; 608 Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { 609 @Override 610 public boolean apply(AccessibilityEvent t) { 611 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { 612 return packageName == null || packageName.equals(t.getPackageName()); 613 } 614 return false; 615 } 616 }; 617 try { 618 getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( 619 emptyRunnable, checkWindowUpdate, timeout); 620 } catch (TimeoutException e) { 621 return false; 622 } catch (Exception e) { 623 Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); 624 return false; 625 } 626 return true; 627 } 628} 629