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