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