UiDevice.java revision 835cffbc85a560a2454fd417073a127895335122
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.os.Build;
22import android.os.Environment;
23import android.os.RemoteException;
24import android.os.ServiceManager;
25import android.os.SystemClock;
26import android.util.DisplayMetrics;
27import android.util.Log;
28import android.view.Display;
29import android.view.IWindowManager;
30import android.view.KeyEvent;
31import android.view.Surface;
32import android.view.WindowManagerImpl;
33import android.view.accessibility.AccessibilityEvent;
34import android.view.accessibility.AccessibilityNodeInfo;
35
36import com.android.internal.statusbar.IStatusBarService;
37import com.android.internal.util.Predicate;
38
39import java.io.File;
40import java.util.ArrayList;
41import java.util.HashMap;
42import java.util.List;
43import java.util.concurrent.TimeoutException;
44
45/**
46 * UiDevice provides access to device wide states. Also provides methods to simulate
47 * pressing hardware buttons such as DPad or the soft buttons such as Home and Menu.
48 */
49public class UiDevice {
50    private static final String LOG_TAG = UiDevice.class.getSimpleName();
51
52    private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
53
54    // store for registered UiWatchers
55    private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>();
56    private final List<String> mWatchersTriggers = new ArrayList<String>();
57
58    // remember if we're executing in the context of a UiWatcher
59    private boolean mInWatcherContext = false;
60
61    // provides access the {@link QueryController} and {@link InteractionController}
62    private final UiAutomatorBridge mUiAutomationBridge;
63
64    // reference to self
65    private static UiDevice mDevice;
66
67    private UiDevice() {
68        mUiAutomationBridge = new UiAutomatorBridge();
69        mDevice = this;
70    }
71
72    boolean isInWatcherContext() {
73        return mInWatcherContext;
74    }
75
76    /**
77     * Provides access the {@link QueryController} and {@link InteractionController}
78     * @return {@link UiAutomatorBridge}
79     */
80    UiAutomatorBridge getAutomatorBridge() {
81        return mUiAutomationBridge;
82    }
83    /**
84     * Allow both the direct creation of a UiDevice and retrieving a existing
85     * instance of UiDevice. This helps tests and their libraries to have access
86     * to UiDevice with necessitating having to always pass copies of UiDevice
87     * instances around.
88     * @return UiDevice instance
89     */
90    public static UiDevice getInstance() {
91        if (mDevice == null) {
92            mDevice = new UiDevice();
93        }
94        return mDevice;
95    }
96
97    /**
98     * Returns the display size in dp (device-independent pixel)
99     *
100     * The returned display size is adjusted per screen rotation
101     *
102     * @return
103     */
104    public Point getDisplaySizeDp() {
105        Display display = WindowManagerImpl.getDefault().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 = WindowManagerImpl.getDefault().getDefaultDisplay();
283        return display.getWidth();
284    }
285
286    /**
287     * Gets the height of the display, in pixels. The size is adjusted based
288     * on the current orientation of the display.
289     * @return height in pixels or zero on failure
290     */
291    public int getDisplayHeight() {
292        Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
293        return display.getHeight();
294    }
295
296    /**
297     * Perform a click at arbitrary coordinates specified by the user
298     *
299     * @param x coordinate
300     * @param y coordinate
301     * @return true if the click succeeded else false
302     */
303    public boolean click(int x, int y) {
304        if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
305            return (false);
306        }
307        return getAutomatorBridge().getInteractionController().click(x, y);
308    }
309
310    /**
311     * Performs a swipe from one coordinate to another using the number of steps
312     * to determine smoothness and speed. Each step execution is throttled to 5ms
313     * per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
314     *
315     * @param startX
316     * @param startY
317     * @param endX
318     * @param endY
319     * @param steps is the number of move steps sent to the system
320     * @return false if the operation fails or the coordinates are invalid
321     */
322    public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
323        return mUiAutomationBridge.getInteractionController()
324                .scrollSwipe(startX, startY, endX, endY, steps);
325    }
326
327    /**
328     * Performs a swipe between points in the Point array. Each step execution is throttled
329     * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete
330     *
331     * @param segments is Point array containing at least one Point object
332     * @param segmentSteps steps to inject between two Points
333     * @return true on success
334     */
335    public boolean swipe(Point[] segments, int segmentSteps) {
336        return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
337    }
338
339    public void waitForIdle() {
340        waitForIdle(DEFAULT_TIMEOUT_MILLIS);
341    }
342
343    public void waitForIdle(long time) {
344        mUiAutomationBridge.waitForIdle(time);
345    }
346
347    /**
348     * Last activity to report accessibility events
349     * @return String name of activity
350     */
351    public String getCurrentActivityName() {
352        return mUiAutomationBridge.getQueryController().getCurrentActivityName();
353    }
354
355    /**
356     * Last package to report accessibility events
357     * @return String name of package
358     */
359    public String getCurrentPackageName() {
360        return mUiAutomationBridge.getQueryController().getCurrentPackageName();
361    }
362
363    /**
364     * Registers a condition watcher to be called by the automation library only when a
365     * {@link UiObject} method call is in progress and is in retry waiting to match
366     * its UI element. Only during these conditions the watchers are invoked to check if
367     * there is something else unexpected on the screen that may be causing the match failure
368     * and retries. Under normal conditions when UiObject methods are immediately matching
369     * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()}
370     *
371     * @param name of watcher
372     * @param watcher {@link UiWatcher}
373     */
374    public void registerWatcher(String name, UiWatcher watcher) {
375        if (mInWatcherContext) {
376            throw new IllegalStateException("Cannot register new watcher from within another");
377        }
378        mWatchers.put(name, watcher);
379    }
380
381    /**
382     * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}.
383     *
384     * @param name of watcher used when <code>registerWatcher</code> was called.
385     * @throws UiAutomationException
386     */
387    public void removeWatcher(String name) {
388        if (mInWatcherContext) {
389            throw new IllegalStateException("Cannot remove a watcher from within another");
390        }
391        mWatchers.remove(name);
392    }
393
394    /**
395     * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers
396     * to run.
397     */
398    public void runWatchers() {
399        if (mInWatcherContext) {
400            return;
401        }
402
403        for (String watcherName : mWatchers.keySet()) {
404            UiWatcher watcher = mWatchers.get(watcherName);
405            if (watcher != null) {
406                try {
407                    mInWatcherContext = true;
408                    if (watcher.checkForCondition()) {
409                        setWatcherTriggered(watcherName);
410                    }
411                } catch (Exception e) {
412                    Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
413                } finally {
414                    mInWatcherContext = false;
415                }
416            }
417        }
418    }
419
420    /**
421     * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and
422     * returns true from its implementation of {@link UiWatcher#checkForCondition()} then
423     * it is considered triggered.
424     */
425    public void resetWatcherTriggers() {
426        mWatchersTriggers.clear();
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. This method can be used to check if a specific UiWatcher
433     * has been triggered during the test. This is helpful if a watcher is detecting errors
434     * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered.
435     */
436    public boolean hasWatcherTriggered(String watcherName) {
437        return mWatchersTriggers.contains(watcherName);
438    }
439
440    /**
441     * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)}
442     */
443    public boolean hasAnyWatcherTriggered() {
444        return mWatchersTriggers.size() > 0;
445    }
446
447    private void setWatcherTriggered(String watcherName) {
448        if (!hasWatcherTriggered(watcherName)) {
449            mWatchersTriggers.add(watcherName);
450        }
451    }
452
453    /**
454     * Check if the device is in its natural orientation. This is determined by checking if the
455     * orientation is at 0 or 180 degrees.
456     * @return true if it is in natural orientation
457     */
458    public boolean isNaturalOrientation() {
459        Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
460        return display.getRotation() == Surface.ROTATION_0 ||
461                display.getRotation() == Surface.ROTATION_180;
462    }
463
464    /**
465     * Disables the sensors and freezes the device rotation at its
466     * current rotation state.
467     * @throws RemoteException
468     */
469    public void freezeRotation() throws RemoteException {
470        getAutomatorBridge().getInteractionController().freezeRotation();
471    }
472
473    /**
474     * Re-enables the sensors and un-freezes the device rotation allowing its contents
475     * to rotate with the device physical rotation. Note that by un-freezing the rotation,
476     * the screen contents may suddenly rotate depending on the current physical position
477     * of the test device. During a test execution, it is best to keep the device frozen
478     * in a specific orientation until the test case execution is completed.
479     * @throws RemoteException
480     */
481    public void unfreezeRotation() throws RemoteException {
482        getAutomatorBridge().getInteractionController().unfreezeRotation();
483    }
484
485    /**
486     * Orients the device to the left and also freezes rotation in that
487     * orientation by disabling the sensors. If you want to un-freeze the rotation
488     * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing
489     * so may cause the screen contents to get re-oriented depending on the current
490     * physical position of the test device.
491     * @throws RemoteException
492     */
493    public void setOrientationLeft() throws RemoteException {
494        getAutomatorBridge().getInteractionController().setRotationLeft();
495    }
496
497    /**
498     * Orients the device to the right 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 setOrientationRight() throws RemoteException {
506        getAutomatorBridge().getInteractionController().setRotationRight();
507    }
508
509    /**
510     * Rotates right and also freezes rotation in that orientation by
511     * disabling the sensors. If you want to un-freeze the rotation
512     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
513     * that doing so may cause the screen contents to rotate
514     * depending on the current physical position of the test device.
515     * @throws RemoteException
516     */
517    public void setOrientationNatural() throws RemoteException {
518        getAutomatorBridge().getInteractionController().setRotationNatural();
519    }
520
521    /**
522     * This method simply presses the power button if the screen is OFF else
523     * it does nothing if the screen is already ON. If the screen was OFF and
524     * it just got turned ON, this method will insert a 500ms delay to allow
525     * the device time to wake up and accept input.
526     * @throws RemoteException
527     */
528    public void wakeUp() throws RemoteException {
529        if(getAutomatorBridge().getInteractionController().wakeDevice()) {
530            // sync delay to allow the window manager to start accepting input
531            // after the device is awakened.
532            SystemClock.sleep(500);
533        }
534    }
535
536    /**
537     * Checks the power manager if the screen is ON
538     * @return true if the screen is ON else false
539     * @throws RemoteException
540     */
541    public boolean isScreenOn() throws RemoteException {
542        return getAutomatorBridge().getInteractionController().isScreenOn();
543    }
544
545    /**
546     * This method simply presses the power button if the screen is ON else
547     * it does nothing if the screen is already OFF.
548     * @throws RemoteException
549     */
550    public void sleep() throws RemoteException {
551        getAutomatorBridge().getInteractionController().sleepDevice();
552    }
553
554    /**
555     * Helper method used for debugging to dump the current window's layout hierarchy.
556     * The file root location is /data/local/tmp
557     *
558     * @param fileName
559     */
560    public void dumpWindowHierarchy(String fileName) {
561        AccessibilityNodeInfo root =
562                getAutomatorBridge().getQueryController().getAccessibilityRootNode();
563        if(root != null) {
564            AccessibilityNodeInfoDumper.dumpWindowToFile(
565                    root, new File(new File(Environment.getDataDirectory(),
566                            "local/tmp"), fileName));
567        }
568    }
569
570    /**
571     * Waits for a window content update event to occur
572     *
573     * if a package name for window is specified, but current window is not with the same package
574     * name, the function will return immediately
575     *
576     * @param packageName the specified window package name; maybe <code>null</code>, and a window
577     *                    update from any frontend window will end the wait
578     * @param timeout the timeout for the wait
579     *
580     * @return true if a window update occured, false if timeout has reached or current window is
581     * not the specified package name
582     */
583    public boolean waitForWindowUpdate(final String packageName, long timeout) {
584        if (packageName != null) {
585            if (!packageName.equals(getCurrentPackageName())) {
586                return false;
587            }
588        }
589        Runnable emptyRunnable = new Runnable() {
590            @Override
591            public void run() {
592            }
593        };
594        Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
595            @Override
596            public boolean apply(AccessibilityEvent t) {
597                if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
598                    return packageName == null || packageName.equals(t.getPackageName());
599                }
600                return false;
601            }
602        };
603        try {
604            getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent(
605                    emptyRunnable, checkWindowUpdate, timeout);
606        } catch (TimeoutException e) {
607            return false;
608        } catch (Exception e) {
609            Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e);
610            return false;
611        }
612        return true;
613    }
614}
615