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