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