UiDevice.java revision 65430f29b4305e4dd8ea42650d40896f34486518
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.Bitmap; 21import android.graphics.Canvas; 22import android.graphics.Matrix; 23import android.graphics.Point; 24import android.hardware.display.DisplayManagerGlobal; 25import android.os.Build; 26import android.os.Environment; 27import android.os.RemoteException; 28import android.os.ServiceManager; 29import android.os.SystemClock; 30import android.util.DisplayMetrics; 31import android.util.Log; 32import android.view.Display; 33import android.view.KeyEvent; 34import android.view.Surface; 35import android.view.accessibility.AccessibilityEvent; 36import android.view.accessibility.AccessibilityNodeInfo; 37 38import com.android.internal.statusbar.IStatusBarService; 39import com.android.internal.util.Predicate; 40 41import java.io.File; 42import java.io.FileOutputStream; 43import java.io.IOException; 44import java.util.ArrayList; 45import java.util.HashMap; 46import java.util.List; 47import java.util.concurrent.TimeoutException; 48 49/** 50 * UiDevice provides access to state information about the device. 51 * You can also use this class to simulate user actions on the device, 52 * such as pressing the d-pad or pressing the Home and Menu buttons. 53 */ 54public class UiDevice { 55 private static final String LOG_TAG = UiDevice.class.getSimpleName(); 56 57 private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000; 58 59 // Sometimes HOME and BACK key presses will generate no events if already on 60 // home page or there is nothing to go back to, Set low timeouts. 61 private static final long KEY_PRESS_EVENT_TIMEOUT = 1 * 1000; 62 63 // store for registered UiWatchers 64 private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>(); 65 private final List<String> mWatchersTriggers = new ArrayList<String>(); 66 67 // remember if we're executing in the context of a UiWatcher 68 private boolean mInWatcherContext = false; 69 70 // provides access the {@link QueryController} and {@link InteractionController} 71 private final UiAutomatorBridge mUiAutomationBridge; 72 73 // reference to self 74 private static UiDevice mDevice; 75 76 private UiDevice() { 77 mUiAutomationBridge = new UiAutomatorBridge(); 78 mDevice = this; 79 } 80 81 boolean isInWatcherContext() { 82 return mInWatcherContext; 83 } 84 85 /** 86 * Provides access the {@link QueryController} and {@link InteractionController} 87 * @return {@link UiAutomatorBridge} 88 */ 89 UiAutomatorBridge getAutomatorBridge() { 90 return mUiAutomationBridge; 91 } 92 /** 93 * Retrieves a singleton instance of UiDevice 94 * 95 * @return UiDevice instance 96 */ 97 public static UiDevice getInstance() { 98 if (mDevice == null) { 99 mDevice = new UiDevice(); 100 } 101 return mDevice; 102 } 103 104 /** 105 * Returns the display size in dp (device-independent pixel) 106 * 107 * The returned display size is adjusted per screen rotation 108 * 109 * @return a Point containing the display size in dp 110 * @hide 111 */ 112 public Point getDisplaySizeDp() { 113 Display display = getDefaultDisplay(); 114 Point p = new Point(); 115 display.getSize(p); 116 DisplayMetrics metrics = new DisplayMetrics(); 117 display.getMetrics(metrics); 118 float dpx = p.x / metrics.density; 119 float dpy = p.y / metrics.density; 120 p.x = Math.round(dpx); 121 p.y = Math.round(dpy); 122 return p; 123 } 124 125 /** 126 * Retrieves the product name of the device. 127 * 128 * This method provides information on what type of device the 129 * test is running on. If you are trying to test for different types of 130 * UI screen sizes, your test should use 131 * {@link UiDevice#getDisplaySizeDp()} instead. This value is the same 132 * returned by invoking #adb shell getprop ro.product.name. 133 * 134 * @return product name of the device 135 */ 136 public String getProductName() { 137 return Build.PRODUCT; 138 } 139 140 /** 141 * Retrieves the text from the last UI traversal event received. 142 * 143 * You can use this method to read the contents in a WebView container 144 * because the accessibility framework fires events 145 * as each text is highlighted. You can write a test to perform 146 * directional arrow presses to focus on different elements inside a WebView, 147 * and call this method to get the text from each traversed element. 148 * If you are testing a view container that can return a reference to a 149 * Document Object Model (DOM) object, your test should use the view's 150 * DOM instead. 151 * 152 * @return text of the last traversal event, else return an empty string 153 */ 154 public String getLastTraversedText() { 155 return mUiAutomationBridge.getQueryController().getLastTraversedText(); 156 } 157 158 /** 159 * Clears the text from the last UI traversal event. 160 * See {@link #getLastTraversedText()}. 161 */ 162 public void clearLastTraversedText() { 163 mUiAutomationBridge.getQueryController().clearLastTraversedText(); 164 } 165 166 /** 167 * Simulates a short press on the MENU button. 168 * @return true if successful, else return false 169 */ 170 public boolean pressMenu() { 171 waitForIdle(); 172 return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent( 173 KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 174 KEY_PRESS_EVENT_TIMEOUT); 175 } 176 177 /** 178 * Simulates a short press on the BACK button. 179 * @return true if successful, else return false 180 */ 181 public boolean pressBack() { 182 waitForIdle(); 183 return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent( 184 KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 185 KEY_PRESS_EVENT_TIMEOUT); 186 } 187 188 /** 189 * Simulates a short press on the HOME button. 190 * @return true if successful, else return false 191 */ 192 public boolean pressHome() { 193 waitForIdle(); 194 return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent( 195 KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 196 KEY_PRESS_EVENT_TIMEOUT); 197 } 198 199 /** 200 * Simulates a short press on the SEARCH button. 201 * @return true if successful, else return false 202 */ 203 public boolean pressSearch() { 204 return pressKeyCode(KeyEvent.KEYCODE_SEARCH); 205 } 206 207 /** 208 * Simulates a short press on the CENTER button. 209 * @return true if successful, else return false 210 */ 211 public boolean pressDPadCenter() { 212 return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER); 213 } 214 215 /** 216 * Simulates a short press on the DOWN button. 217 * @return true if successful, else return false 218 */ 219 public boolean pressDPadDown() { 220 return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN); 221 } 222 223 /** 224 * Simulates a short press on the UP button. 225 * @return true if successful, else return false 226 */ 227 public boolean pressDPadUp() { 228 return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP); 229 } 230 231 /** 232 * Simulates a short press on the LEFT button. 233 * @return true if successful, else return false 234 */ 235 public boolean pressDPadLeft() { 236 return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT); 237 } 238 239 /** 240 * Simulates a short press on the RIGHT button. 241 * @return true if successful, else return false 242 */ 243 public boolean pressDPadRight() { 244 return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT); 245 } 246 247 /** 248 * Simulates a short press on the DELETE key. 249 * @return true if successful, else return false 250 */ 251 public boolean pressDelete() { 252 return pressKeyCode(KeyEvent.KEYCODE_DEL); 253 } 254 255 /** 256 * Simulates a short press on the ENTER key. 257 * @return true if successful, else return false 258 */ 259 public boolean pressEnter() { 260 return pressKeyCode(KeyEvent.KEYCODE_ENTER); 261 } 262 263 /** 264 * Simulates a short press using a key code. 265 * 266 * See {@link KeyEvent} 267 * @return true if successful, else return false 268 */ 269 public boolean pressKeyCode(int keyCode) { 270 waitForIdle(); 271 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0); 272 } 273 274 /** 275 * Simulates a short press using a key code. 276 * 277 * See {@link KeyEvent}. 278 * @param keyCode the key code of the event. 279 * @param metaState an integer in which each bit set to 1 represents a pressed meta key 280 * @return true if successful, else return false 281 */ 282 public boolean pressKeyCode(int keyCode, int metaState) { 283 waitForIdle(); 284 return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState); 285 } 286 287 /** 288 * Simulates a short press on the Recent Apps button. 289 * 290 * @return true if successful, else return false 291 * @throws RemoteException 292 */ 293 public boolean pressRecentApps() throws RemoteException { 294 waitForIdle(); 295 final IStatusBarService statusBar = IStatusBarService.Stub.asInterface( 296 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 297 298 if (statusBar != null) { 299 statusBar.toggleRecentApps(); 300 return true; 301 } 302 return false; 303 } 304 305 /** 306 * Gets the width of the display, in pixels. The width and height details 307 * are reported based on the current orientation of the display. 308 * @return width in pixels or zero on failure 309 */ 310 public int getDisplayWidth() { 311 Display display = getDefaultDisplay(); 312 Point p = new Point(); 313 display.getSize(p); 314 return p.x; 315 } 316 317 /** 318 * Gets the height of the display, in pixels. The size is adjusted based 319 * on the current orientation of the display. 320 * @return height in pixels or zero on failure 321 */ 322 public int getDisplayHeight() { 323 Display display = getDefaultDisplay(); 324 Point p = new Point(); 325 display.getSize(p); 326 return p.y; 327 } 328 329 /** 330 * Perform a click at arbitrary coordinates specified by the user 331 * 332 * @param x coordinate 333 * @param y coordinate 334 * @return true if the click succeeded else false 335 */ 336 public boolean click(int x, int y) { 337 if (x >= getDisplayWidth() || y >= getDisplayHeight()) { 338 return (false); 339 } 340 return getAutomatorBridge().getInteractionController().click(x, y); 341 } 342 343 /** 344 * Performs a swipe from one coordinate to another using the number of steps 345 * to determine smoothness and speed. Each step execution is throttled to 5ms 346 * per step. So for a 100 steps, the swipe will take about 1/2 second to complete. 347 * 348 * @param startX 349 * @param startY 350 * @param endX 351 * @param endY 352 * @param steps is the number of move steps sent to the system 353 * @return false if the operation fails or the coordinates are invalid 354 */ 355 public boolean swipe(int startX, int startY, int endX, int endY, int steps) { 356 return mUiAutomationBridge.getInteractionController() 357 .scrollSwipe(startX, startY, endX, endY, steps); 358 } 359 360 /** 361 * Performs a swipe between points in the Point array. Each step execution is throttled 362 * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete 363 * 364 * @param segments is Point array containing at least one Point object 365 * @param segmentSteps steps to inject between two Points 366 * @return true on success 367 */ 368 public boolean swipe(Point[] segments, int segmentSteps) { 369 return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps); 370 } 371 372 /** 373 * Waits for the current application to idle. 374 * Default wait timeout is 10 seconds 375 */ 376 public void waitForIdle() { 377 waitForIdle(DEFAULT_TIMEOUT_MILLIS); 378 } 379 380 /** 381 * Waits for the current application to idle. 382 * @param timeout in milliseconds 383 */ 384 public void waitForIdle(long time) { 385 mUiAutomationBridge.waitForIdle(time); 386 } 387 388 /** 389 * Retrieves the last activity to report accessibility events. 390 * @return String name of activity 391 */ 392 public String getCurrentActivityName() { 393 return mUiAutomationBridge.getQueryController().getCurrentActivityName(); 394 } 395 396 /** 397 * Retrieves the name of the last package to report accessibility events. 398 * @return String name of package 399 */ 400 public String getCurrentPackageName() { 401 return mUiAutomationBridge.getQueryController().getCurrentPackageName(); 402 } 403 404 /** 405 * Registers a {@link UiWatcher} to run automatically when the testing framework is unable to 406 * find a match using a {@link UiSelector}. See {@link #runWatchers()} 407 * 408 * @param name to register the UiWatcher 409 * @param watcher {@link UiWatcher} 410 */ 411 public void registerWatcher(String name, UiWatcher watcher) { 412 if (mInWatcherContext) { 413 throw new IllegalStateException("Cannot register new watcher from within another"); 414 } 415 mWatchers.put(name, watcher); 416 } 417 418 /** 419 * Removes a previously registered {@link UiWatcher}. 420 * 421 * See {@link #registerWatcher(String, UiWatcher)} 422 * @param name used to register the UiWatcher 423 * @throws UiAutomationException 424 */ 425 public void removeWatcher(String name) { 426 if (mInWatcherContext) { 427 throw new IllegalStateException("Cannot remove a watcher from within another"); 428 } 429 mWatchers.remove(name); 430 } 431 432 /** 433 * This method forces all registered watchers to run. 434 * See {@link #registerWatcher(String, UiWatcher)} 435 */ 436 public void runWatchers() { 437 if (mInWatcherContext) { 438 return; 439 } 440 441 for (String watcherName : mWatchers.keySet()) { 442 UiWatcher watcher = mWatchers.get(watcherName); 443 if (watcher != null) { 444 try { 445 mInWatcherContext = true; 446 if (watcher.checkForCondition()) { 447 setWatcherTriggered(watcherName); 448 } 449 } catch (Exception e) { 450 Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e); 451 } finally { 452 mInWatcherContext = false; 453 } 454 } 455 } 456 } 457 458 /** 459 * Resets a {@link UiWatcher} that has been triggered. 460 * If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call 461 * returned <code>true</code>, then the UiWatcher is considered triggered. 462 * See {@link #registerWatcher(String, UiWatcher)} 463 */ 464 public void resetWatcherTriggers() { 465 mWatchersTriggers.clear(); 466 } 467 468 /** 469 * Checks if a specific registered {@link UiWatcher} has triggered. 470 * See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its 471 * {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then 472 * the UiWatcher is considered triggered. This is helpful if a watcher is detecting errors 473 * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered. 474 * 475 * @param watcherName 476 * @return true if triggered else false 477 */ 478 public boolean hasWatcherTriggered(String watcherName) { 479 return mWatchersTriggers.contains(watcherName); 480 } 481 482 /** 483 * Checks if any registered {@link UiWatcher} have triggered. 484 * 485 * See {@link #registerWatcher(String, UiWatcher)} 486 * See {@link #hasWatcherTriggered(String)} 487 */ 488 public boolean hasAnyWatcherTriggered() { 489 return mWatchersTriggers.size() > 0; 490 } 491 492 /** 493 * Used internally by this class to set a {@link UiWatcher} state as triggered. 494 * @param watcherName 495 */ 496 private void setWatcherTriggered(String watcherName) { 497 if (!hasWatcherTriggered(watcherName)) { 498 mWatchersTriggers.add(watcherName); 499 } 500 } 501 502 /** 503 * Check if the device is in its natural orientation. This is determined by checking if the 504 * orientation is at 0 or 180 degrees. 505 * @return true if it is in natural orientation 506 */ 507 public boolean isNaturalOrientation() { 508 Display display = getDefaultDisplay(); 509 return display.getRotation() == Surface.ROTATION_0 || 510 display.getRotation() == Surface.ROTATION_180; 511 } 512 513 /** 514 * Returns the current rotation of the display, as defined in {@link Surface} 515 * @return 516 */ 517 public int getDisplayRotation() { 518 return getDefaultDisplay().getRotation(); 519 } 520 521 /** 522 * Disables the sensors and freezes the device rotation at its 523 * current rotation state. 524 * @throws RemoteException 525 */ 526 public void freezeRotation() throws RemoteException { 527 getAutomatorBridge().getInteractionController().freezeRotation(); 528 } 529 530 /** 531 * Re-enables the sensors and un-freezes the device rotation allowing its contents 532 * to rotate with the device physical rotation. During a test execution, it is best to 533 * keep the device frozen in a specific orientation until the test case execution has completed. 534 * @throws RemoteException 535 */ 536 public void unfreezeRotation() throws RemoteException { 537 getAutomatorBridge().getInteractionController().unfreezeRotation(); 538 } 539 540 /** 541 * Simulates orienting the device to the left and also freezes rotation 542 * by disabling the sensors. 543 * 544 * If you want to un-freeze the rotation and re-enable the sensors 545 * see {@link #unfreezeRotation()}. 546 * @throws RemoteException 547 */ 548 public void setOrientationLeft() throws RemoteException { 549 getAutomatorBridge().getInteractionController().setRotationLeft(); 550 } 551 552 /** 553 * Simulates orienting the device to the right and also freezes rotation 554 * by disabling the sensors. 555 * 556 * If you want to un-freeze the rotation and re-enable the sensors 557 * see {@link #unfreezeRotation()}. 558 * @throws RemoteException 559 */ 560 public void setOrientationRight() throws RemoteException { 561 getAutomatorBridge().getInteractionController().setRotationRight(); 562 } 563 564 /** 565 * Simulates orienting the device into its natural orientation and also freezes rotation 566 * by disabling the sensors. 567 * 568 * If you want to un-freeze the rotation and re-enable the sensors 569 * see {@link #unfreezeRotation()}. 570 * @throws RemoteException 571 */ 572 public void setOrientationNatural() throws RemoteException { 573 getAutomatorBridge().getInteractionController().setRotationNatural(); 574 } 575 576 /** 577 * This method simulates pressing the power button if the screen is OFF else 578 * it does nothing if the screen is already ON. 579 * 580 * If the screen was OFF and it just got turned ON, this method will insert a 500ms delay 581 * to allow the device time to wake up and accept input. 582 * @throws RemoteException 583 */ 584 public void wakeUp() throws RemoteException { 585 if(getAutomatorBridge().getInteractionController().wakeDevice()) { 586 // sync delay to allow the window manager to start accepting input 587 // after the device is awakened. 588 SystemClock.sleep(500); 589 } 590 } 591 592 /** 593 * Checks the power manager if the screen is ON. 594 * 595 * @return true if the screen is ON else false 596 * @throws RemoteException 597 */ 598 public boolean isScreenOn() throws RemoteException { 599 return getAutomatorBridge().getInteractionController().isScreenOn(); 600 } 601 602 /** 603 * This method simply presses the power button if the screen is ON else 604 * it does nothing if the screen is already OFF. 605 * 606 * @throws RemoteException 607 */ 608 public void sleep() throws RemoteException { 609 getAutomatorBridge().getInteractionController().sleepDevice(); 610 } 611 612 /** 613 * Helper method used for debugging to dump the current window's layout hierarchy. 614 * The file root location is /data/local/tmp 615 * 616 * @param fileName 617 */ 618 public void dumpWindowHierarchy(String fileName) { 619 AccessibilityNodeInfo root = 620 getAutomatorBridge().getQueryController().getAccessibilityRootNode(); 621 if(root != null) { 622 AccessibilityNodeInfoDumper.dumpWindowToFile( 623 root, new File(new File(Environment.getDataDirectory(), 624 "local/tmp"), fileName)); 625 } 626 } 627 628 /** 629 * Waits for a window content update event to occur. 630 * 631 * If a package name for the window is specified, but the current window 632 * does not have the same package name, the function returns immediately. 633 * 634 * @param packageName the specified window package name (can be <code>null</code>). 635 * If <code>null</code>, a window update from any front-end window will end the wait 636 * @param timeout the timeout for the wait 637 * 638 * @return true if a window update occurred, false if timeout has elapsed or if the current 639 * window does not have the specified package name 640 */ 641 public boolean waitForWindowUpdate(final String packageName, long timeout) { 642 if (packageName != null) { 643 if (!packageName.equals(getCurrentPackageName())) { 644 return false; 645 } 646 } 647 Runnable emptyRunnable = new Runnable() { 648 @Override 649 public void run() { 650 } 651 }; 652 Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() { 653 @Override 654 public boolean apply(AccessibilityEvent t) { 655 if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { 656 return packageName == null || packageName.equals(t.getPackageName()); 657 } 658 return false; 659 } 660 }; 661 try { 662 getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent( 663 emptyRunnable, checkWindowUpdate, timeout); 664 } catch (TimeoutException e) { 665 return false; 666 } catch (Exception e) { 667 Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e); 668 return false; 669 } 670 return true; 671 } 672 673 private static Display getDefaultDisplay() { 674 return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY); 675 } 676 677 /** 678 * @return the current display rotation in degrees 679 */ 680 private static float getDegreesForRotation(int value) { 681 switch (value) { 682 case Surface.ROTATION_90: 683 return 360f - 90f; 684 case Surface.ROTATION_180: 685 return 360f - 180f; 686 case Surface.ROTATION_270: 687 return 360f - 270f; 688 } 689 return 0f; 690 } 691 692 /** 693 * Take a screenshot of current window and store it as PNG 694 * 695 * Default scale of 1.0f (original size) and 90% quality is used 696 * 697 * @param storePath where the PNG should be written to 698 * @return 699 */ 700 public boolean takeScreenshot(File storePath) { 701 return takeScreenshot(storePath, 1.0f, 90); 702 } 703 704 /** 705 * Take a screenshot of current window and store it as PNG 706 * 707 * The screenshot is adjusted per screen rotation; 708 * 709 * @param storePath where the PNG should be written to 710 * @param scale scale the screenshot down if needed; 1.0f for original size 711 * @param quality quality of the PNG compression; range: 0-100 712 * @return 713 */ 714 public boolean takeScreenshot(File storePath, float scale, int quality) { 715 // This is from com.android.systemui.screenshot.GlobalScreenshot#takeScreenshot 716 // We need to orient the screenshot correctly (and the Surface api seems to take screenshots 717 // only in the natural orientation of the device :!) 718 DisplayMetrics displayMetrics = new DisplayMetrics(); 719 Display display = getDefaultDisplay(); 720 display.getRealMetrics(displayMetrics); 721 float[] dims = {displayMetrics.widthPixels, displayMetrics.heightPixels}; 722 float degrees = getDegreesForRotation(display.getRotation()); 723 boolean requiresRotation = (degrees > 0); 724 Matrix matrix = new Matrix(); 725 matrix.reset(); 726 if (scale != 1.0f) { 727 matrix.setScale(scale, scale); 728 } 729 if (requiresRotation) { 730 // Get the dimensions of the device in its native orientation 731 matrix.preRotate(-degrees); 732 } 733 matrix.mapPoints(dims); 734 dims[0] = Math.abs(dims[0]); 735 dims[1] = Math.abs(dims[1]); 736 737 // Take the screenshot 738 Bitmap screenShot = Surface.screenshot((int) dims[0], (int) dims[1]); 739 if (screenShot == null) { 740 return false; 741 } 742 743 if (requiresRotation) { 744 // Rotate the screenshot to the current orientation 745 int width = displayMetrics.widthPixels; 746 int height = displayMetrics.heightPixels; 747 if (scale != 1.0f) { 748 width = Math.round(scale * width); 749 height = Math.round(scale * height); 750 } 751 Bitmap ss = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 752 Canvas c = new Canvas(ss); 753 c.translate(ss.getWidth() / 2, ss.getHeight() / 2); 754 c.rotate(degrees); 755 c.translate(-dims[0] / 2, -dims[1] / 2); 756 c.drawBitmap(screenShot, 0, 0, null); 757 c.setBitmap(null); 758 screenShot = ss; 759 } 760 761 // Optimizations 762 screenShot.setHasAlpha(false); 763 764 try { 765 FileOutputStream fos = new FileOutputStream(storePath); 766 screenShot.compress(Bitmap.CompressFormat.PNG, quality, fos); 767 fos.flush(); 768 fos.close(); 769 } catch (IOException ioe) { 770 Log.e(LOG_TAG, "failed to save screen shot to file", ioe); 771 return false; 772 } finally { 773 screenShot.recycle(); 774 } 775 return true; 776 } 777} 778