UiDevice.java revision 835cffbc85a560a2454fd417073a127895335122
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 * 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 = WindowManagerImpl.getDefault().getDefaultDisplay(); 283 return display.getWidth(); 284 } 285 286 /** 287 * Gets the height of the display, in pixels. The size is adjusted based 288 * on the current orientation of the display. 289 * @return height in pixels or zero on failure 290 */ 291 public int getDisplayHeight() { 292 Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); 293 return display.getHeight(); 294 } 295 296 /** 297 * Perform a click at arbitrary coordinates specified by the user 298 * 299 * @param x coordinate 300 * @param y coordinate 301 * @return true if the click succeeded else false 302 */ 303 public boolean click(int x, int y) { 304 if (x >= getDisplayWidth() || y >= getDisplayHeight()) { 305 return (false); 306 } 307 return getAutomatorBridge().getInteractionController().click(x, y); 308 } 309 310 /** 311 * Performs a swipe from one coordinate to another using the number of steps 312 * to determine smoothness and speed. Each step execution is throttled to 5ms 313 * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. 314 * 315 * @param startX 316 * @param startY 317 * @param endX 318 * @param endY 319 * @param steps is the number of move steps sent to the system 320 * @return false if the operation fails or the coordinates are invalid 321 */ 322 public boolean swipe(int startX, int startY, int endX, int endY, int steps) { 323 return mUiAutomationBridge.getInteractionController() 324 .scrollSwipe(startX, startY, endX, endY, steps); 325 } 326 327 /** 328 * Performs a swipe between points in the Point array. Each step execution is throttled 329 * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete 330 * 331 * @param segments is Point array containing at least one Point object 332 * @param segmentSteps steps to inject between two Points 333 * @return true on success 334 */ 335 public boolean swipe(Point[] segments, int segmentSteps) { 336 return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); 337 } 338 339 public void waitForIdle() { 340 waitForIdle(DEFAULT_TIMEOUT_MILLIS); 341 } 342 343 public void waitForIdle(long time) { 344 mUiAutomationBridge.waitForIdle(time); 345 } 346 347 /** 348 * Last activity to report accessibility events 349 * @return String name of activity 350 */ 351 public String getCurrentActivityName() { 352 return mUiAutomationBridge.getQueryController().getCurrentActivityName(); 353 } 354 355 /** 356 * Last package to report accessibility events 357 * @return String name of package 358 */ 359 public String getCurrentPackageName() { 360 return mUiAutomationBridge.getQueryController().getCurrentPackageName(); 361 } 362 363 /** 364 * Registers a condition watcher to be called by the automation library only when a 365 * {@link UiObject} method call is in progress and is in retry waiting to match 366 * its UI element. Only during these conditions the watchers are invoked to check if 367 * there is something else unexpected on the screen that may be causing the match failure 368 * and retries. Under normal conditions when UiObject methods are immediately matching 369 * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()} 370 * 371 * @param name of watcher 372 * @param watcher {@link UiWatcher} 373 */ 374 public void registerWatcher(String name, UiWatcher watcher) { 375 if (mInWatcherContext) { 376 throw new IllegalStateException("Cannot register new watcher from within another"); 377 } 378 mWatchers.put(name, watcher); 379 } 380 381 /** 382 * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}. 383 * 384 * @param name of watcher used when <code>registerWatcher</code> was called. 385 * @throws UiAutomationException 386 */ 387 public void removeWatcher(String name) { 388 if (mInWatcherContext) { 389 throw new IllegalStateException("Cannot remove a watcher from within another"); 390 } 391 mWatchers.remove(name); 392 } 393 394 /** 395 * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers 396 * to run. 397 */ 398 public void runWatchers() { 399 if (mInWatcherContext) { 400 return; 401 } 402 403 for (String watcherName : mWatchers.keySet()) { 404 UiWatcher watcher = mWatchers.get(watcherName); 405 if (watcher != null) { 406 try { 407 mInWatcherContext = true; 408 if (watcher.checkForCondition()) { 409 setWatcherTriggered(watcherName); 410 } 411 } catch (Exception e) { 412 Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); 413 } finally { 414 mInWatcherContext = false; 415 } 416 } 417 } 418 } 419 420 /** 421 * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and 422 * returns true from its implementation of {@link UiWatcher#checkForCondition()} then 423 * it is considered triggered. 424 */ 425 public void resetWatcherTriggers() { 426 mWatchersTriggers.clear(); 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. This method can be used to check if a specific UiWatcher 433 * has been triggered during the test. This is helpful if a watcher is detecting errors 434 * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. 435 */ 436 public boolean hasWatcherTriggered(String watcherName) { 437 return mWatchersTriggers.contains(watcherName); 438 } 439 440 /** 441 * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)} 442 */ 443 public boolean hasAnyWatcherTriggered() { 444 return mWatchersTriggers.size() > 0; 445 } 446 447 private void setWatcherTriggered(String watcherName) { 448 if (!hasWatcherTriggered(watcherName)) { 449 mWatchersTriggers.add(watcherName); 450 } 451 } 452 453 /** 454 * Check if the device is in its natural orientation. This is determined by checking if the 455 * orientation is at 0 or 180 degrees. 456 * @return true if it is in natural orientation 457 */ 458 public boolean isNaturalOrientation() { 459 Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); 460 return display.getRotation() == Surface.ROTATION_0 || 461 display.getRotation() == Surface.ROTATION_180; 462 } 463 464 /** 465 * Disables the sensors and freezes the device rotation at its 466 * current rotation state. 467 * @throws RemoteException 468 */ 469 public void freezeRotation() throws RemoteException { 470 getAutomatorBridge().getInteractionController().freezeRotation(); 471 } 472 473 /** 474 * Re-enables the sensors and un-freezes the device rotation allowing its contents 475 * to rotate with the device physical rotation. Note that by un-freezing the rotation, 476 * the screen contents may suddenly rotate depending on the current physical position 477 * of the test device. During a test execution, it is best to keep the device frozen 478 * in a specific orientation until the test case execution is completed. 479 * @throws RemoteException 480 */ 481 public void unfreezeRotation() throws RemoteException { 482 getAutomatorBridge().getInteractionController().unfreezeRotation(); 483 } 484 485 /** 486 * Orients the device to the left and also freezes rotation in that 487 * orientation by disabling the sensors. If you want to un-freeze the rotation 488 * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing 489 * so may cause the screen contents to get re-oriented depending on the current 490 * physical position of the test device. 491 * @throws RemoteException 492 */ 493 public void setOrientationLeft() throws RemoteException { 494 getAutomatorBridge().getInteractionController().setRotationLeft(); 495 } 496 497 /** 498 * Orients the device to the right 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 setOrientationRight() throws RemoteException { 506 getAutomatorBridge().getInteractionController().setRotationRight(); 507 } 508 509 /** 510 * Rotates right and also freezes rotation in that orientation by 511 * disabling the sensors. If you want to un-freeze the rotation 512 * and re-enable the sensors see {@link #unfreezeRotation()}. Note 513 * that doing so may cause the screen contents to rotate 514 * depending on the current physical position of the test device. 515 * @throws RemoteException 516 */ 517 public void setOrientationNatural() throws RemoteException { 518 getAutomatorBridge().getInteractionController().setRotationNatural(); 519 } 520 521 /** 522 * This method simply presses the power button if the screen is OFF else 523 * it does nothing if the screen is already ON. If the screen was OFF and 524 * it just got turned ON, this method will insert a 500ms delay to allow 525 * the device time to wake up and accept input. 526 * @throws RemoteException 527 */ 528 public void wakeUp() throws RemoteException { 529 if(getAutomatorBridge().getInteractionController().wakeDevice()) { 530 // sync delay to allow the window manager to start accepting input 531 // after the device is awakened. 532 SystemClock.sleep(500); 533 } 534 } 535 536 /** 537 * Checks the power manager if the screen is ON 538 * @return true if the screen is ON else false 539 * @throws RemoteException 540 */ 541 public boolean isScreenOn() throws RemoteException { 542 return getAutomatorBridge().getInteractionController().isScreenOn(); 543 } 544 545 /** 546 * This method simply presses the power button if the screen is ON else 547 * it does nothing if the screen is already OFF. 548 * @throws RemoteException 549 */ 550 public void sleep() throws RemoteException { 551 getAutomatorBridge().getInteractionController().sleepDevice(); 552 } 553 554 /** 555 * Helper method used for debugging to dump the current window's layout hierarchy. 556 * The file root location is /data/local/tmp 557 * 558 * @param fileName 559 */ 560 public void dumpWindowHierarchy(String fileName) { 561 AccessibilityNodeInfo root = 562 getAutomatorBridge().getQueryController().getAccessibilityRootNode(); 563 if(root != null) { 564 AccessibilityNodeInfoDumper.dumpWindowToFile( 565 root, new File(new File(Environment.getDataDirectory(), 566 "local/tmp"), fileName)); 567 } 568 } 569 570 /** 571 * Waits for a window content update event to occur 572 * 573 * if a package name for window is specified, but current window is not with the same package 574 * name, the function will return immediately 575 * 576 * @param packageName the specified window package name; maybe <code>null</code>, and a window 577 * update from any frontend window will end the wait 578 * @param timeout the timeout for the wait 579 * 580 * @return true if a window update occured, false if timeout has reached or current window is 581 * not the specified package name 582 */ 583 public boolean waitForWindowUpdate(final String packageName, long timeout) { 584 if (packageName != null) { 585 if (!packageName.equals(getCurrentPackageName())) { 586 return false; 587 } 588 } 589 Runnable emptyRunnable = new Runnable() { 590 @Override 591 public void run() { 592 } 593 }; 594 Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { 595 @Override 596 public boolean apply(AccessibilityEvent t) { 597 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { 598 return packageName == null || packageName.equals(t.getPackageName()); 599 } 600 return false; 601 } 602 }; 603 try { 604 getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( 605 emptyRunnable, checkWindowUpdate, timeout); 606 } catch (TimeoutException e) { 607 return false; 608 } catch (Exception e) { 609 Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); 610 return false; 611 } 612 return true; 613 } 614} 615