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