UiDevice.java revision 467cca7d25dbbf13c14cfd8c2ad38ab8eaf56bda
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     * Gets the width of the display, in pixels. The width and height details
261     * are reported based on the current orientation of the display.
262     * @return width in pixels or zero on failure
263     */
264    public int getDisplayWidth() {
265        IWindowManager wm = IWindowManager.Stub.asInterface(
266                ServiceManager.getService(Context.WINDOW_SERVICE));
267        Point p = new Point();
268        try {
269            wm.getDisplaySize(p);
270        } catch (RemoteException e) {
271            return 0;
272        }
273        return p.x;
274    }
275
276    /**
277     * Press recent apps soft key
278     * @return true if successful
279     * @throws RemoteException
280     */
281    public boolean pressRecentApps() throws RemoteException {
282        waitForIdle();
283        final IStatusBarService statusBar = IStatusBarService.Stub.asInterface(
284                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
285
286        if (statusBar != null) {
287            statusBar.toggleRecentApps();
288            return true;
289        }
290        return false;
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        IWindowManager wm = IWindowManager.Stub.asInterface(
300                ServiceManager.getService(Context.WINDOW_SERVICE));
301        Point p = new Point();
302        try {
303            wm.getDisplaySize(p);
304        } catch (RemoteException e) {
305            return 0;
306        }
307        return p.y;
308    }
309
310    /**
311     * Perform a click at arbitrary coordinates specified by the user
312     *
313     * @param x coordinate
314     * @param y coordinate
315     * @return true if the click succeeded else false
316     */
317    public boolean click(int x, int y) {
318        if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
319            return (false);
320        }
321        return getAutomatorBridge().getInteractionController().click(x, y);
322    }
323
324    /**
325     * Performs a swipe from one coordinate to another using the number of steps
326     * to determine smoothness and speed. Each step execution is throttled to 5ms
327     * per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
328     *
329     * @param startX
330     * @param startY
331     * @param endX
332     * @param endY
333     * @param steps is the number of move steps sent to the system
334     * @return false if the operation fails or the coordinates are invalid
335     */
336    public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
337        return mUiAutomationBridge.getInteractionController()
338                .scrollSwipe(startX, startY, endX, endY, steps);
339    }
340
341    /**
342     * Performs a swipe between points in the Point array. Each step execution is throttled
343     * to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete
344     *
345     * @param segments is Point array containing at least one Point object
346     * @param segmentSteps steps to inject between two Points
347     * @return true on success
348     */
349    public boolean swipe(Point[] segments, int segmentSteps) {
350        return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
351    }
352
353    public void waitForIdle() {
354        waitForIdle(DEFAULT_TIMEOUT_MILLIS);
355    }
356
357    public void waitForIdle(long time) {
358        mUiAutomationBridge.waitForIdle(time);
359    }
360
361    /**
362     * Last activity to report accessibility events
363     * @return String name of activity
364     */
365    public String getCurrentActivityName() {
366        return mUiAutomationBridge.getQueryController().getCurrentActivityName();
367    }
368
369    /**
370     * Last package to report accessibility events
371     * @return String name of package
372     */
373    public String getCurrentPackageName() {
374        return mUiAutomationBridge.getQueryController().getCurrentPackageName();
375    }
376
377    /**
378     * Registers a condition watcher to be called by the automation library only when a
379     * {@link UiObject} method call is in progress and is in retry waiting to match
380     * its UI element. Only during these conditions the watchers are invoked to check if
381     * there is something else unexpected on the screen that may be causing the match failure
382     * and retries. Under normal conditions when UiObject methods are immediately matching
383     * their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()}
384     *
385     * @param name of watcher
386     * @param watcher {@link UiWatcher}
387     */
388    public void registerWatcher(String name, UiWatcher watcher) {
389        if (mInWatcherContext) {
390            throw new IllegalStateException("Cannot register new watcher from within another");
391        }
392        mWatchers.put(name, watcher);
393    }
394
395    /**
396     * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}.
397     *
398     * @param name of watcher used when <code>registerWatcher</code> was called.
399     * @throws UiAutomationException
400     */
401    public void removeWatcher(String name) {
402        if (mInWatcherContext) {
403            throw new IllegalStateException("Cannot remove a watcher from within another");
404        }
405        mWatchers.remove(name);
406    }
407
408    /**
409     * See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers
410     * to run.
411     */
412    public void runWatchers() {
413        if (mInWatcherContext) {
414            return;
415        }
416
417        for (String watcherName : mWatchers.keySet()) {
418            UiWatcher watcher = mWatchers.get(watcherName);
419            if (watcher != null) {
420                try {
421                    mInWatcherContext = true;
422                    if (watcher.checkForCondition()) {
423                        setWatcherTriggered(watcherName);
424                    }
425                } catch (Exception e) {
426                    Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
427                } finally {
428                    mInWatcherContext = false;
429                }
430            }
431        }
432    }
433
434    /**
435     * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and
436     * returns true from its implementation of {@link UiWatcher#checkForCondition()} then
437     * it is considered triggered.
438     */
439    public void resetWatcherTriggers() {
440        mWatchersTriggers.clear();
441    }
442
443    /**
444     * See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and
445     * returns true from its implementation of {@link UiWatcher#checkForCondition()} then
446     * it is considered triggered. This method can be used to check if a specific UiWatcher
447     * has been triggered during the test. This is helpful if a watcher is detecting errors
448     * from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered.
449     */
450    public boolean hasWatcherTriggered(String watcherName) {
451        return mWatchersTriggers.contains(watcherName);
452    }
453
454    /**
455     * See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)}
456     */
457    public boolean hasAnyWatcherTriggered() {
458        return mWatchersTriggers.size() > 0;
459    }
460
461    private void setWatcherTriggered(String watcherName) {
462        if (!hasWatcherTriggered(watcherName)) {
463            mWatchersTriggers.add(watcherName);
464        }
465    }
466
467    /**
468     * Check if the device is in its natural orientation. This is determined by checking if the
469     * orientation is at 0 or 180 degrees.
470     * @return true if it is in natural orientation
471     */
472    public boolean isNaturalOrientation() {
473        Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
474        return display.getRotation() == Surface.ROTATION_0 ||
475                display.getRotation() == Surface.ROTATION_180;
476    }
477
478    /**
479     * Disables the sensors and freezes the device rotation at its
480     * current rotation state.
481     * @throws RemoteException
482     */
483    public void freezeRotation() throws RemoteException {
484        getAutomatorBridge().getInteractionController().freezeRotation();
485    }
486
487    /**
488     * Re-enables the sensors and un-freezes the device rotation allowing its contents
489     * to rotate with the device physical rotation. Note that by un-freezing the rotation,
490     * the screen contents may suddenly rotate depending on the current physical position
491     * of the test device. During a test execution, it is best to keep the device frozen
492     * in a specific orientation until the test case execution is completed.
493     * @throws RemoteException
494     */
495    public void unfreezeRotation() throws RemoteException {
496        getAutomatorBridge().getInteractionController().unfreezeRotation();
497    }
498
499    /**
500     * Orients the device to the left and also freezes rotation in that
501     * orientation by disabling the sensors. If you want to un-freeze the rotation
502     * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing
503     * so may cause the screen contents to get re-oriented depending on the current
504     * physical position of the test device.
505     * @throws RemoteException
506     */
507    public void setOrientationLeft() throws RemoteException {
508        getAutomatorBridge().getInteractionController().setRotationLeft();
509    }
510
511    /**
512     * Orients the device to the right and also freezes rotation in that
513     * orientation by disabling the sensors. If you want to un-freeze the rotation
514     * and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing
515     * so may cause the screen contents to get re-oriented depending on the current
516     * physical position of the test device.
517     * @throws RemoteException
518     */
519    public void setOrientationRight() throws RemoteException {
520        getAutomatorBridge().getInteractionController().setRotationRight();
521    }
522
523    /**
524     * Rotates right and also freezes rotation in that orientation by
525     * disabling the sensors. If you want to un-freeze the rotation
526     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
527     * that doing so may cause the screen contents to rotate
528     * depending on the current physical position of the test device.
529     * @throws RemoteException
530     */
531    public void setOrientationNatural() throws RemoteException {
532        getAutomatorBridge().getInteractionController().setRotationNatural();
533    }
534
535    /**
536     * This method simply presses the power button if the screen is OFF else
537     * it does nothing if the screen is already ON. If the screen was OFF and
538     * it just got turned ON, this method will insert a 500ms delay to allow
539     * the device time to wake up and accept input.
540     * @throws RemoteException
541     */
542    public void wakeUp() throws RemoteException {
543        if(getAutomatorBridge().getInteractionController().wakeDevice()) {
544            // sync delay to allow the window manager to start accepting input
545            // after the device is awakened.
546            SystemClock.sleep(500);
547        }
548    }
549
550    /**
551     * Checks the power manager if the screen is ON
552     * @return true if the screen is ON else false
553     * @throws RemoteException
554     */
555    public boolean isScreenOn() throws RemoteException {
556        return getAutomatorBridge().getInteractionController().isScreenOn();
557    }
558
559    /**
560     * This method simply presses the power button if the screen is ON else
561     * it does nothing if the screen is already OFF.
562     * @throws RemoteException
563     */
564    public void sleep() throws RemoteException {
565        getAutomatorBridge().getInteractionController().sleepDevice();
566    }
567
568    /**
569     * Helper method used for debugging to dump the current window's layout hierarchy.
570     * The file root location is /data/local/tmp
571     *
572     * @param fileName
573     */
574    public void dumpWindowHierarchy(String fileName) {
575        AccessibilityNodeInfo root =
576                getAutomatorBridge().getQueryController().getAccessibilityRootNode();
577        if(root != null) {
578            AccessibilityNodeInfoDumper.dumpWindowToFile(
579                    root, new File(new File(Environment.getDataDirectory(),
580                            "local/tmp"), fileName));
581        }
582    }
583
584    /**
585     * Waits for a window content update event to occur
586     *
587     * if a package name for window is specified, but current window is not with the same package
588     * name, the function will return immediately
589     *
590     * @param packageName the specified window package name; maybe <code>null</code>, and a window
591     *                    update from any frontend window will end the wait
592     * @param timeout the timeout for the wait
593     *
594     * @return true if a window update occured, false if timeout has reached or current window is
595     * not the specified package name
596     */
597    public boolean waitForWindowUpdate(final String packageName, long timeout) {
598        if (packageName != null) {
599            if (!packageName.equals(getCurrentPackageName())) {
600                return false;
601            }
602        }
603        Runnable emptyRunnable = new Runnable() {
604            @Override
605            public void run() {
606            }
607        };
608        Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
609            @Override
610            public boolean apply(AccessibilityEvent t) {
611                if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
612                    return packageName == null || packageName.equals(t.getPackageName());
613                }
614                return false;
615            }
616        };
617        try {
618            getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent(
619                    emptyRunnable, checkWindowUpdate, timeout);
620        } catch (TimeoutException e) {
621            return false;
622        } catch (Exception e) {
623            Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e);
624            return false;
625        }
626        return true;
627    }
628}
629