UiDevice.java revision 4ab790eccf6d5c27f542056b87d26d38f7caeeb3
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.DisplayMetrics; 26import android.util.Log; 27import android.view.Display; 28import android.view.IWindowManager; 29import android.view.KeyEvent; 30import android.view.Surface; 31import android.view.WindowManagerImpl; 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 Boolean mIsPhone = null; 67 68 private UiDevice() { 69 mUiAutomationBridge = new UiAutomatorBridge(); 70 mDevice = this; 71 } 72 73 boolean isInWatcherContext() { 74 return mInWatcherContext; 75 } 76 77 /** 78 * Provides access the {@link QueryController} and {@link InteractionController} 79 * @return {@link UiAutomatorBridge} 80 */ 81 UiAutomatorBridge getAutomatorBridge() { 82 return mUiAutomationBridge; 83 } 84 /** 85 * Allow both the direct creation of a UiDevice and retrieving a existing 86 * instance of UiDevice. This helps tests and their libraries to have access 87 * to UiDevice with necessitating having to always pass copies of UiDevice 88 * instances around. 89 * @return UiDevice instance 90 */ 91 public static UiDevice getInstance() { 92 if (mDevice == null) { 93 mDevice = new UiDevice(); 94 } 95 return mDevice; 96 } 97 98 /** 99 * This forces the return value of {@link #isPhone()} to be a specific device type. 100 * For example, on certain devices the {@link #isPhone} may return true when an application 101 * is actually behaving as if it is on a tablet. For these types of devices, it would be 102 * best if the test forces the issue by invoking this method accordingly. 103 * @param val true for phone behavior else false for all other 104 */ 105 public void setTypeAsPhone(boolean val) { 106 mIsPhone = val; 107 } 108 109 /** 110 * Check if the tests are running on a phone screen. This method assumes a 111 * phone is a device that its natural rotation has a height > width or when 112 * rotated it has a width > height. This API is deprecated. Use the UI to 113 * determine the layout. For example if on larger screen devices your app displays 114 * two ListViews but on a small screen one, then count the ListViews to decide. see 115 * {@link UiObject#getMatchesCount()} 116 * @return true if the device has a phone else false 117 */ 118 @Deprecated 119 public boolean isPhone() { 120 if(mIsPhone == null) { 121 DisplayMetrics metrics = new DisplayMetrics(); 122 Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); 123 display.getMetrics(metrics); 124 125 if(isOrientationNatural()) { 126 // we assume a phone has a natural orientation that has height > width 127 if(metrics.heightPixels > metrics.widthPixels) 128 return true; 129 } else { 130 // we assume a phone has a rotated orientation that has height < width 131 if(metrics.heightPixels < metrics.widthPixels) 132 return true; 133 } 134 135 // not a phone 136 return false; 137 } 138 139 return mIsPhone; 140 } 141 142 /** 143 * Check the current device orientation 144 * @return true if in natural orientation 145 */ 146 public boolean isOrientationNatural() { 147 Display display = WindowManagerImpl.getDefault().getDefaultDisplay(); 148 return display.getRotation() == Surface.ROTATION_0 || 149 display.getRotation() == Surface.ROTATION_180; 150 } 151 152 /** 153 * Every event received from accessibility may or may not contain text. This 154 * method returns the text from the last UI traversal event received that had text. 155 * This is helpful in web views when the test performs down arrow presses to focus 156 * on different elements inside the web view, the accessibility will fire events 157 * with the text just highlighted. In effect once can read the contents of a 158 * web view this way. 159 * @return text of the last traversal event else an empty string 160 */ 161 public String getLastTraversedText() { 162 return mUiAutomationBridge.getQueryController().getLastTraversedText(); 163 } 164 165 /** 166 * Helper to clear the text saved of the last accessibility UI traversal event that had 167 * any text in it. See {@link #getLastTraversedText()}. 168 */ 169 public void clearLastTraversedText() { 170 mUiAutomationBridge.getQueryController().clearLastTraversedText(); 171 } 172 173 /** 174 * Helper method to do a short press on MENU button 175 * @return true if successful else false 176 */ 177 public boolean pressMenu() { 178 return pressKeyCode(KeyEvent.KEYCODE_MENU); 179 } 180 181 /** 182 * Helper method to do a short press on BACK button 183 * @return true if successful else false 184 */ 185 public boolean pressBack() { 186 return pressKeyCode(KeyEvent.KEYCODE_BACK); 187 } 188 189 /** 190 * Helper method to do a short press on HOME button 191 * @return true if successful else false 192 */ 193 public boolean pressHome() { 194 return pressKeyCode(KeyEvent.KEYCODE_HOME); 195 } 196 197 /** 198 * Helper method to do a short press on SEARCH button 199 * @return true if successful else false 200 */ 201 public boolean pressSearch() { 202 return pressKeyCode(KeyEvent.KEYCODE_SEARCH); 203 } 204 205 /** 206 * Helper method to do a short press on DOWN button 207 * @return true if successful else false 208 */ 209 public boolean pressDPadCenter() { 210 return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); 211 } 212 213 /** 214 * Helper method to do a short press on DOWN button 215 * @return true if successful else false 216 */ 217 public boolean pressDPadDown() { 218 return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); 219 } 220 221 /** 222 * Helper method to do a short press on UP button 223 * @return true if successful else false 224 */ 225 public boolean pressDPadUp() { 226 return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); 227 } 228 229 /** 230 * Helper method to do a short press on LEFT button 231 * @return true if successful else false 232 */ 233 public boolean pressDPadLeft() { 234 return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); 235 } 236 237 /** 238 * Helper method to do a short press on RIGTH button 239 * @return true if successful else false 240 */ 241 public boolean pressDPadRight() { 242 return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); 243 } 244 245 /** 246 * Helper method to do a short press on DELETE 247 * @return true if successful else false 248 */ 249 public boolean pressDelete() { 250 return pressKeyCode(KeyEvent.KEYCODE_DEL); 251 } 252 253 /** 254 * Helper method to do a short press on ENTER 255 * @return true if successful else false 256 */ 257 public boolean pressEnter() { 258 return pressKeyCode(KeyEvent.KEYCODE_ENTER); 259 } 260 261 /** 262 * Helper method to do a short press using a key code. See {@link KeyEvent} 263 * @return true if successful else false 264 */ 265 public boolean pressKeyCode(int keyCode) { 266 waitForIdle(); 267 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0); 268 } 269 270 /** 271 * Helper method to do a short press using a key code. See {@link KeyEvent} 272 * @param keyCode See {@link KeyEvent} 273 * @param metaState See {@link KeyEvent} 274 * @return true if successful else false 275 */ 276 public boolean pressKeyCode(int keyCode, int metaState) { 277 waitForIdle(); 278 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState); 279 } 280 281 /** 282 * Gets the raw width of the display, in pixels. The size is adjusted based 283 * on the current rotation of the display. 284 * @return width in pixels or zero on failure 285 */ 286 public int getDisplayWidth() { 287 IWindowManager wm = IWindowManager.Stub.asInterface( 288 ServiceManager.getService(Context.WINDOW_SERVICE)); 289 Point p = new Point(); 290 try { 291 wm.getDisplaySize(p); 292 } catch (RemoteException e) { 293 return 0; 294 } 295 return p.x; 296 } 297 298 /** 299 * Press recent apps soft key 300 * @return true if successful 301 * @throws RemoteException 302 */ 303 public boolean pressRecentApps() throws RemoteException { 304 waitForIdle(); 305 final IStatusBarService statusBar = IStatusBarService.Stub.asInterface( 306 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 307 308 if (statusBar != null) { 309 statusBar.toggleRecentApps(); 310 return true; 311 } 312 return false; 313 } 314 315 /** 316 * Gets the raw height of the display, in pixels. The size is adjusted based 317 * on the current rotation of the display. 318 * @return height in pixels or zero on failure 319 */ 320 public int getDisplayHeight() { 321 IWindowManager wm = IWindowManager.Stub.asInterface( 322 ServiceManager.getService(Context.WINDOW_SERVICE)); 323 Point p = new Point(); 324 try { 325 wm.getDisplaySize(p); 326 } catch (RemoteException e) { 327 return 0; 328 } 329 return p.y; 330 } 331 332 /** 333 * Perform a click at arbitrary coordinates specified by the user 334 * @param x coordinate 335 * @param y coordinate 336 * @return true if the click succeeded else false 337 */ 338 public boolean click(int x, int y) { 339 if (x >= getDisplayWidth() || y >= getDisplayHeight()) { 340 return (false); 341 } 342 return getAutomatorBridge().getInteractionController().click(x, y); 343 } 344 345 /** 346 * Performs a swipe from one coordinate to another using the number of steps 347 * to determine smoothness and speed. The more steps the slower and smoother 348 * the swipe will be. 349 * @param startX 350 * @param startY 351 * @param endX 352 * @param endY 353 * @param steps is the number of move steps sent to the system 354 * @return false if the operation fails or the coordinates are invalid 355 */ 356 public boolean swipe(int startX, int startY, int endX, int endY, int steps) { 357 return mUiAutomationBridge.getInteractionController() 358 .scrollSwipe(startX, startY, endX, endY, steps); 359 } 360 361 /** 362 * Performs a swipe between points in the Point array. 363 * @param segments is Point array containing at least one Point object 364 * @param segmentSteps steps to inject between two Points 365 * @return true on success 366 */ 367 public boolean swipe(Point[] segments, int segmentSteps) { 368 return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); 369 } 370 371 public void waitForIdle() { 372 waitForIdle(DEFAULT_TIMEOUT_MILLIS); 373 } 374 375 public void waitForIdle(long time) { 376 mUiAutomationBridge.waitForIdle(time); 377 } 378 379 /** 380 * Last activity to report accessibility events 381 * @return String name of activity 382 */ 383 public String getCurrentActivityName() { 384 return mUiAutomationBridge.getQueryController().getCurrentActivityName(); 385 } 386 387 /** 388 * Last package to report accessibility events 389 * @return String name of package 390 */ 391 public String getCurrentPackageName() { 392 return mUiAutomationBridge.getQueryController().getCurrentPackageName(); 393 } 394 395 396 /** 397 * Enables the test script to register a condition watcher to be called by 398 * the automation library. The automation library will invoke 399 * {@link UiWatcher#checkForCondition} only when a regular API call is in 400 * retry mode when it is unable to locate its selector yet. Only during this 401 * time, the watchers are invoked to check if there is something else 402 * unexpected on the screen that may be causing the delay in detecting the 403 * required UI object. 404 * @param name of watcher 405 * @param watcher {@link UiWatcher} 406 */ 407 public void registerWatcher(String name, UiWatcher watcher) { 408 if (mInWatcherContext) { 409 throw new IllegalStateException("Cannot register new watcher from within another"); 410 } 411 mWatchers.put(name, watcher); 412 } 413 414 /** 415 * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}. 416 * @param name of watcher used when <code>registerWatcher</code> was called. 417 * @throws UiAutomationException 418 */ 419 public void removeWatcher(String name) { 420 if (mInWatcherContext) { 421 throw new IllegalStateException("Cannot remove a watcher from within another"); 422 } 423 mWatchers.remove(name); 424 } 425 426 /** 427 * Watchers are generally not run unless a certain UI object is not being 428 * found. This will help improve performance of tests until there is a good 429 * reason to check for possible exceptions on the display.<b/><b/> However, 430 * in some cases it may be desirable to force run the watchers. Calling this 431 * method will execute all registered watchers. 432 */ 433 public void runWatchers() { 434 if (mInWatcherContext) { 435 return; 436 } 437 438 for (String watcherName : mWatchers.keySet()) { 439 UiWatcher watcher = mWatchers.get(watcherName); 440 if (watcher != null) { 441 try { 442 mInWatcherContext = true; 443 if (watcher.checkForCondition()) { 444 setWatcherTriggered(watcherName); 445 } 446 } catch (Exception e) { 447 Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); 448 } finally { 449 mInWatcherContext = false; 450 } 451 } 452 } 453 } 454 455 /** 456 * If you have used {@link #registerWatcher(String, UiWatcher)} then this 457 * method can be used to reset reported UiWatcher triggers. 458 * A {@link UiWatcher} reports it is triggered by returning true 459 * from its implementation of {@link UiWatcher#checkForCondition()} 460 */ 461 public void resetWatcherTriggers() { 462 mWatchersTriggers.clear(); 463 } 464 465 /** 466 * If you have used {@link #registerWatcher(String, UiWatcher)} then this 467 * method can be used to check if a specific UiWatcher has ever triggered during the 468 * test. For a {@link UiWatcher} to report it is triggered it needs to return true 469 * from its implementation of {@link UiWatcher#checkForCondition()} 470 */ 471 public boolean hasWatcherTriggered(String watcherName) { 472 return mWatchersTriggers.contains(watcherName); 473 } 474 475 /** 476 * If you have used {@link #registerWatcher(String, UiWatcher)} then this 477 * method can be used to check if any of those have ever triggered during the 478 * test. For a {@link UiWatcher} to report it is triggered it needs to return true 479 * from its implementation of {@link UiWatcher#checkForCondition()} 480 */ 481 public boolean hasAnyWatcherTriggered() { 482 return mWatchersTriggers.size() > 0; 483 } 484 485 private void setWatcherTriggered(String watcherName) { 486 if (!hasWatcherTriggered(watcherName)) { 487 mWatchersTriggers.add(watcherName); 488 } 489 } 490 491 /** 492 * Check if the device is in its natural orientation. This is determined by 493 * checking whether the orientation is at 0 or 180 degrees. 494 * @return true if it is in natural orientation 495 * @throws RemoteException 496 */ 497 public boolean isNaturalRotation() throws RemoteException { 498 return getAutomatorBridge().getInteractionController().isNaturalRotation(); 499 } 500 501 /** 502 * Disables the sensors and freezes the device rotation at its 503 * current rotation state. 504 * @throws RemoteException 505 */ 506 public void freezeRotation() throws RemoteException { 507 getAutomatorBridge().getInteractionController().freezeRotation(); 508 } 509 510 /** 511 * Re-enables the sensors and un-freezes the device rotation 512 * allowing its contents to rotate with the device physical rotation. 513 * @throws RemoteException 514 */ 515 public void unfreezeRotation() throws RemoteException { 516 getAutomatorBridge().getInteractionController().unfreezeRotation(); 517 } 518 519 /** 520 * Rotates left and also freezes rotation in that position by 521 * disabling the sensors. If you want to un-freeze the rotation 522 * and re-enable the sensors see {@link #unfreezeRotation()}. Note 523 * that doing so may cause the screen contents to rotate 524 * depending on the current physical position of the test device. 525 * @throws RemoteException 526 */ 527 public void setRotationLeft() throws RemoteException { 528 getAutomatorBridge().getInteractionController().setRotationLeft(); 529 } 530 531 /** 532 * Rotates right and also freezes rotation in that position by 533 * disabling the sensors. If you want to un-freeze the rotation 534 * and re-enable the sensors see {@link #unfreezeRotation()}. Note 535 * that doing so may cause the screen contents to rotate 536 * depending on the current physical position of the test device. 537 * @throws RemoteException 538 */ 539 public void setRotationRight() throws RemoteException { 540 getAutomatorBridge().getInteractionController().setRotationRight(); 541 } 542 543 /** 544 * Check if the device is in its natural orientation. This is determined by 545 * checking whether the orientation is at 0 or 180 degrees. 546 * @return true if it is in natural orientation 547 * @throws RemoteException 548 */ 549 public void setRotationNatural() throws RemoteException { 550 getAutomatorBridge().getInteractionController().setRotationNatural(); 551 } 552 553 /** 554 * This method simply presses the power button if the screen is OFF else 555 * it does nothing if the screen is already ON. If the screen was OFF and 556 * it just got turned ON, this method will insert a 500ms delay to allow 557 * the device time to wake up and accept input. 558 * @throws RemoteException 559 */ 560 public void wakeUp() throws RemoteException { 561 if(getAutomatorBridge().getInteractionController().wakeDevice()) { 562 // sync delay to allow the window manager to start accepting input 563 // after the device is awakened. 564 SystemClock.sleep(500); 565 } 566 } 567 568 /** 569 * Checks the power manager if the screen is ON 570 * @return true if the screen is ON else false 571 * @throws RemoteException 572 */ 573 public boolean isScreenOn() throws RemoteException { 574 return getAutomatorBridge().getInteractionController().isScreenOn(); 575 } 576 577 /** 578 * This method simply presses the power button if the screen is ON else 579 * it does nothing if the screen is already OFF. 580 * @throws RemoteException 581 */ 582 public void sleep() throws RemoteException { 583 getAutomatorBridge().getInteractionController().sleepDevice(); 584 } 585 586 /** 587 * Helper method used for debugging to dump the current window's layout hierarchy. 588 * The file root location is /data/local/tmp 589 * @param fileName 590 */ 591 public void dumpWindowHierarchy(String fileName) { 592 AccessibilityNodeInfo root = 593 getAutomatorBridge().getQueryController().getAccessibilityRootNode(); 594 if(root != null) { 595 AccessibilityNodeInfoDumper.dumpWindowToFile( 596 root, new File(new File(Environment.getDataDirectory(), 597 "local/tmp"), fileName)); 598 } 599 } 600 601 602 /** 603 * Waits for a window content update event to occur 604 * 605 * if a package name for window is specified, but current window is not with the same package 606 * name, the function will return immediately 607 * 608 * @param packageName the specified window package name; maybe <code>null</code>, and a window 609 * update from any frontend window will end the wait 610 * @param timeout the timeout for the wait 611 * 612 * @return true if a window update occured, false if timeout has reached or current window is 613 * not the specified package name 614 */ 615 public boolean waitForWindowUpdate(final String packageName, long timeout) { 616 if (packageName != null) { 617 if (!packageName.equals(getCurrentPackageName())) { 618 return false; 619 } 620 } 621 Runnable emptyRunnable = new Runnable() { 622 @Override 623 public void run() { 624 } 625 }; 626 Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { 627 @Override 628 public boolean apply(AccessibilityEvent t) { 629 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { 630 return packageName == null || packageName.equals(t.getPackageName()); 631 } 632 return false; 633 } 634 }; 635 try { 636 getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( 637 emptyRunnable, checkWindowUpdate, timeout); 638 } catch (TimeoutException e) { 639 return false; 640 } catch (Exception e) { 641 Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); 642 return false; 643 } 644 return true; 645 } 646} 647