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