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