UiDevice.java revision 4ab790eccf6d5c27f542056b87d26d38f7caeeb3
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.DisplayMetrics;
26import android.util.Log;
27import android.view.Display;
28import android.view.IWindowManager;
29import android.view.KeyEvent;
30import android.view.Surface;
31import android.view.WindowManagerImpl;
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 Boolean mIsPhone = null;
67
68    private UiDevice() {
69        mUiAutomationBridge = new UiAutomatorBridge();
70        mDevice = this;
71    }
72
73    boolean isInWatcherContext() {
74        return mInWatcherContext;
75    }
76
77    /**
78     * Provides access the {@link QueryController} and {@link InteractionController}
79     * @return {@link UiAutomatorBridge}
80     */
81    UiAutomatorBridge getAutomatorBridge() {
82        return mUiAutomationBridge;
83    }
84    /**
85     * Allow both the direct creation of a UiDevice and retrieving a existing
86     * instance of UiDevice. This helps tests and their libraries to have access
87     * to UiDevice with necessitating having to always pass copies of UiDevice
88     * instances around.
89     * @return UiDevice instance
90     */
91    public static UiDevice getInstance() {
92        if (mDevice == null) {
93            mDevice = new UiDevice();
94        }
95        return mDevice;
96    }
97
98    /**
99     * This forces the return value of {@link #isPhone()} to be a specific device type.
100     * For example, on certain devices the {@link #isPhone} may return true when an application
101     * is actually behaving as if it is on a tablet. For these types of devices, it would be
102     * best if the test forces the issue by invoking this method accordingly.
103     * @param val true for phone behavior else false for all other
104     */
105    public void setTypeAsPhone(boolean val) {
106        mIsPhone = val;
107    }
108
109    /**
110     * Check if the tests are running on a phone screen. This method assumes a
111     * phone is a device that its natural rotation has a height > width or when
112     * rotated it has a width > height. This API is deprecated. Use the UI to
113     * determine the layout. For example if on larger screen devices your app displays
114     * two ListViews but on a small screen one, then count the ListViews to decide. see
115     * {@link UiObject#getMatchesCount()}
116     * @return true if the device has a phone else false
117     */
118    @Deprecated
119    public boolean isPhone() {
120        if(mIsPhone == null) {
121            DisplayMetrics metrics = new DisplayMetrics();
122            Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
123            display.getMetrics(metrics);
124
125            if(isOrientationNatural()) {
126                // we assume a phone has a natural orientation that has height > width
127                if(metrics.heightPixels > metrics.widthPixels)
128                    return true;
129            } else {
130                // we assume a phone has a rotated orientation that has height < width
131                if(metrics.heightPixels < metrics.widthPixels)
132                    return true;
133            }
134
135            // not a phone
136            return false;
137        }
138
139        return mIsPhone;
140    }
141
142    /**
143     * Check the current device orientation
144     * @return true if in natural orientation
145     */
146    public boolean isOrientationNatural() {
147        Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
148        return display.getRotation() == Surface.ROTATION_0 ||
149                display.getRotation() == Surface.ROTATION_180;
150    }
151
152    /**
153     * Every event received from accessibility may or may not contain text. This
154     * method returns the text from the last UI traversal event received that had text.
155     * This is helpful in web views when the test performs down arrow presses to focus
156     * on different elements inside the web view, the accessibility will fire events
157     * with the text just highlighted. In effect once can read the contents of a
158     * web view this way.
159     * @return text of the last traversal event else an empty string
160     */
161    public String getLastTraversedText() {
162        return mUiAutomationBridge.getQueryController().getLastTraversedText();
163    }
164
165    /**
166     * Helper to clear the text saved of the last accessibility UI traversal event that had
167     * any text in it. See {@link #getLastTraversedText()}.
168     */
169    public void clearLastTraversedText() {
170        mUiAutomationBridge.getQueryController().clearLastTraversedText();
171    }
172
173    /**
174     * Helper method to do a short press on MENU button
175     * @return true if successful else false
176     */
177    public boolean pressMenu() {
178        return pressKeyCode(KeyEvent.KEYCODE_MENU);
179    }
180
181    /**
182     * Helper method to do a short press on BACK button
183     * @return true if successful else false
184     */
185    public boolean pressBack() {
186        return pressKeyCode(KeyEvent.KEYCODE_BACK);
187    }
188
189    /**
190     * Helper method to do a short press on HOME button
191     * @return true if successful else false
192     */
193    public boolean pressHome() {
194        return pressKeyCode(KeyEvent.KEYCODE_HOME);
195    }
196
197    /**
198     * Helper method to do a short press on SEARCH button
199     * @return true if successful else false
200     */
201    public boolean pressSearch() {
202        return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
203    }
204
205    /**
206     * Helper method to do a short press on DOWN button
207     * @return true if successful else false
208     */
209    public boolean pressDPadCenter() {
210        return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
211    }
212
213    /**
214     * Helper method to do a short press on DOWN button
215     * @return true if successful else false
216     */
217    public boolean pressDPadDown() {
218        return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
219    }
220
221    /**
222     * Helper method to do a short press on UP button
223     * @return true if successful else false
224     */
225    public boolean pressDPadUp() {
226        return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
227    }
228
229    /**
230     * Helper method to do a short press on LEFT button
231     * @return true if successful else false
232     */
233    public boolean pressDPadLeft() {
234        return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
235    }
236
237    /**
238     * Helper method to do a short press on RIGTH button
239     * @return true if successful else false
240     */
241    public boolean pressDPadRight() {
242        return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
243    }
244
245    /**
246     * Helper method to do a short press on DELETE
247     * @return true if successful else false
248     */
249    public boolean pressDelete() {
250        return pressKeyCode(KeyEvent.KEYCODE_DEL);
251    }
252
253    /**
254     * Helper method to do a short press on ENTER
255     * @return true if successful else false
256     */
257    public boolean pressEnter() {
258        return pressKeyCode(KeyEvent.KEYCODE_ENTER);
259    }
260
261    /**
262     * Helper method to do a short press using a key code. See {@link KeyEvent}
263     * @return true if successful else false
264     */
265    public boolean pressKeyCode(int keyCode) {
266        waitForIdle();
267        return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0);
268    }
269
270    /**
271     * Helper method to do a short press using a key code. See {@link KeyEvent}
272     * @param keyCode See {@link KeyEvent}
273     * @param metaState See {@link KeyEvent}
274     * @return true if successful else false
275     */
276    public boolean pressKeyCode(int keyCode, int metaState) {
277        waitForIdle();
278        return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState);
279    }
280
281    /**
282     * Gets the raw width of the display, in pixels. The size is adjusted based
283     * on the current rotation of the display.
284     * @return width in pixels or zero on failure
285     */
286    public int getDisplayWidth() {
287        IWindowManager wm = IWindowManager.Stub.asInterface(
288                ServiceManager.getService(Context.WINDOW_SERVICE));
289        Point p = new Point();
290        try {
291            wm.getDisplaySize(p);
292        } catch (RemoteException e) {
293            return 0;
294        }
295        return p.x;
296    }
297
298    /**
299     * Press recent apps soft key
300     * @return true if successful
301     * @throws RemoteException
302     */
303    public boolean pressRecentApps() throws RemoteException {
304        waitForIdle();
305        final IStatusBarService statusBar = IStatusBarService.Stub.asInterface(
306                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
307
308        if (statusBar != null) {
309            statusBar.toggleRecentApps();
310            return true;
311        }
312        return false;
313    }
314
315    /**
316     * Gets the raw height of the display, in pixels. The size is adjusted based
317     * on the current rotation of the display.
318     * @return height in pixels or zero on failure
319     */
320    public int getDisplayHeight() {
321        IWindowManager wm = IWindowManager.Stub.asInterface(
322                ServiceManager.getService(Context.WINDOW_SERVICE));
323        Point p = new Point();
324        try {
325            wm.getDisplaySize(p);
326        } catch (RemoteException e) {
327            return 0;
328        }
329        return p.y;
330    }
331
332    /**
333     * Perform a click at arbitrary coordinates specified by the user
334     * @param x coordinate
335     * @param y coordinate
336     * @return true if the click succeeded else false
337     */
338    public boolean click(int x, int y) {
339        if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
340            return (false);
341        }
342        return getAutomatorBridge().getInteractionController().click(x, y);
343    }
344
345    /**
346     * Performs a swipe from one coordinate to another using the number of steps
347     * to determine smoothness and speed. The more steps the slower and smoother
348     * the swipe will be.
349     * @param startX
350     * @param startY
351     * @param endX
352     * @param endY
353     * @param steps is the number of move steps sent to the system
354     * @return false if the operation fails or the coordinates are invalid
355     */
356    public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
357        return mUiAutomationBridge.getInteractionController()
358                .scrollSwipe(startX, startY, endX, endY, steps);
359    }
360
361    /**
362     * Performs a swipe between points in the Point array.
363     * @param segments is Point array containing at least one Point object
364     * @param segmentSteps steps to inject between two Points
365     * @return true on success
366     */
367    public boolean swipe(Point[] segments, int segmentSteps) {
368        return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
369    }
370
371    public void waitForIdle() {
372        waitForIdle(DEFAULT_TIMEOUT_MILLIS);
373    }
374
375    public void waitForIdle(long time) {
376        mUiAutomationBridge.waitForIdle(time);
377    }
378
379    /**
380     * Last activity to report accessibility events
381     * @return String name of activity
382     */
383    public String getCurrentActivityName() {
384        return mUiAutomationBridge.getQueryController().getCurrentActivityName();
385    }
386
387    /**
388     * Last package to report accessibility events
389     * @return String name of package
390     */
391    public String getCurrentPackageName() {
392        return mUiAutomationBridge.getQueryController().getCurrentPackageName();
393    }
394
395
396    /**
397     * Enables the test script to register a condition watcher to be called by
398     * the automation library. The automation library will invoke
399     * {@link UiWatcher#checkForCondition} only when a regular API call is in
400     * retry mode when it is unable to locate its selector yet. Only during this
401     * time, the watchers are invoked to check if there is something else
402     * unexpected on the screen that may be causing the delay in detecting the
403     * required UI object.
404     * @param name of watcher
405     * @param watcher {@link UiWatcher}
406     */
407    public void registerWatcher(String name, UiWatcher watcher) {
408        if (mInWatcherContext) {
409            throw new IllegalStateException("Cannot register new watcher from within another");
410        }
411        mWatchers.put(name, watcher);
412    }
413
414    /**
415     * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}.
416     * @param name of watcher used when <code>registerWatcher</code> was called.
417     * @throws UiAutomationException
418     */
419    public void removeWatcher(String name) {
420        if (mInWatcherContext) {
421            throw new IllegalStateException("Cannot remove a watcher from within another");
422        }
423        mWatchers.remove(name);
424    }
425
426    /**
427     * Watchers are generally not run unless a certain UI object is not being
428     * found. This will help improve performance of tests until there is a good
429     * reason to check for possible exceptions on the display.<b/><b/> However,
430     * in some cases it may be desirable to force run the watchers. Calling this
431     * method will execute all registered watchers.
432     */
433    public void runWatchers() {
434        if (mInWatcherContext) {
435            return;
436        }
437
438        for (String watcherName : mWatchers.keySet()) {
439            UiWatcher watcher = mWatchers.get(watcherName);
440            if (watcher != null) {
441                try {
442                    mInWatcherContext = true;
443                    if (watcher.checkForCondition()) {
444                        setWatcherTriggered(watcherName);
445                    }
446                } catch (Exception e) {
447                    Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
448                } finally {
449                    mInWatcherContext = false;
450                }
451            }
452        }
453    }
454
455    /**
456     * If you have used {@link #registerWatcher(String, UiWatcher)} then this
457     * method can be used to reset reported UiWatcher triggers.
458     * A {@link UiWatcher} reports it is triggered by returning true
459     * from its implementation of {@link UiWatcher#checkForCondition()}
460     */
461    public void resetWatcherTriggers() {
462        mWatchersTriggers.clear();
463    }
464
465    /**
466     * If you have used {@link #registerWatcher(String, UiWatcher)} then this
467     * method can be used to check if a specific UiWatcher has ever triggered during the
468     * test. For a {@link UiWatcher} to report it is triggered it needs to return true
469     * from its implementation of {@link UiWatcher#checkForCondition()}
470     */
471    public boolean hasWatcherTriggered(String watcherName) {
472        return mWatchersTriggers.contains(watcherName);
473    }
474
475    /**
476     * If you have used {@link #registerWatcher(String, UiWatcher)} then this
477     * method can be used to check if any of those have ever triggered during the
478     * test. For a {@link UiWatcher} to report it is triggered it needs to return true
479     * from its implementation of {@link UiWatcher#checkForCondition()}
480     */
481    public boolean hasAnyWatcherTriggered() {
482        return mWatchersTriggers.size() > 0;
483    }
484
485    private void setWatcherTriggered(String watcherName) {
486        if (!hasWatcherTriggered(watcherName)) {
487            mWatchersTriggers.add(watcherName);
488        }
489    }
490
491    /**
492     * Check if the device is in its natural orientation. This is determined by
493     * checking whether the orientation is at 0 or 180 degrees.
494     * @return true if it is in natural orientation
495     * @throws RemoteException
496     */
497    public boolean isNaturalRotation() throws RemoteException {
498        return getAutomatorBridge().getInteractionController().isNaturalRotation();
499    }
500
501    /**
502     * Disables the sensors and freezes the device rotation at its
503     * current rotation state.
504     * @throws RemoteException
505     */
506    public void freezeRotation() throws RemoteException {
507        getAutomatorBridge().getInteractionController().freezeRotation();
508    }
509
510    /**
511     * Re-enables the sensors and un-freezes the device rotation
512     * allowing its contents to rotate with the device physical rotation.
513     * @throws RemoteException
514     */
515    public void unfreezeRotation() throws RemoteException {
516        getAutomatorBridge().getInteractionController().unfreezeRotation();
517    }
518
519    /**
520     * Rotates left and also freezes rotation in that position by
521     * disabling the sensors. If you want to un-freeze the rotation
522     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
523     * that doing so may cause the screen contents to rotate
524     * depending on the current physical position of the test device.
525     * @throws RemoteException
526     */
527    public void setRotationLeft() throws RemoteException {
528        getAutomatorBridge().getInteractionController().setRotationLeft();
529    }
530
531    /**
532     * Rotates right and also freezes rotation in that position by
533     * disabling the sensors. If you want to un-freeze the rotation
534     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
535     * that doing so may cause the screen contents to rotate
536     * depending on the current physical position of the test device.
537     * @throws RemoteException
538     */
539    public void setRotationRight() throws RemoteException {
540        getAutomatorBridge().getInteractionController().setRotationRight();
541    }
542
543    /**
544     * Check if the device is in its natural orientation. This is determined by
545     * checking whether the orientation is at 0 or 180 degrees.
546     * @return true if it is in natural orientation
547     * @throws RemoteException
548     */
549    public void setRotationNatural() throws RemoteException {
550        getAutomatorBridge().getInteractionController().setRotationNatural();
551    }
552
553    /**
554     * This method simply presses the power button if the screen is OFF else
555     * it does nothing if the screen is already ON. If the screen was OFF and
556     * it just got turned ON, this method will insert a 500ms delay to allow
557     * the device time to wake up and accept input.
558     * @throws RemoteException
559     */
560    public void wakeUp() throws RemoteException {
561        if(getAutomatorBridge().getInteractionController().wakeDevice()) {
562            // sync delay to allow the window manager to start accepting input
563            // after the device is awakened.
564            SystemClock.sleep(500);
565        }
566    }
567
568    /**
569     * Checks the power manager if the screen is ON
570     * @return true if the screen is ON else false
571     * @throws RemoteException
572     */
573    public boolean isScreenOn() throws RemoteException {
574        return getAutomatorBridge().getInteractionController().isScreenOn();
575    }
576
577    /**
578     * This method simply presses the power button if the screen is ON else
579     * it does nothing if the screen is already OFF.
580     * @throws RemoteException
581     */
582    public void sleep() throws RemoteException {
583        getAutomatorBridge().getInteractionController().sleepDevice();
584    }
585
586    /**
587     * Helper method used for debugging to dump the current window's layout hierarchy.
588     * The file root location is /data/local/tmp
589     * @param fileName
590     */
591    public void dumpWindowHierarchy(String fileName) {
592        AccessibilityNodeInfo root =
593                getAutomatorBridge().getQueryController().getAccessibilityRootNode();
594        if(root != null) {
595            AccessibilityNodeInfoDumper.dumpWindowToFile(
596                    root, new File(new File(Environment.getDataDirectory(),
597                            "local/tmp"), fileName));
598        }
599    }
600
601
602    /**
603     * Waits for a window content update event to occur
604     *
605     * if a package name for window is specified, but current window is not with the same package
606     * name, the function will return immediately
607     *
608     * @param packageName the specified window package name; maybe <code>null</code>, and a window
609     *                    update from any frontend window will end the wait
610     * @param timeout the timeout for the wait
611     *
612     * @return true if a window update occured, false if timeout has reached or current window is
613     * not the specified package name
614     */
615    public boolean waitForWindowUpdate(final String packageName, long timeout) {
616        if (packageName != null) {
617            if (!packageName.equals(getCurrentPackageName())) {
618                return false;
619            }
620        }
621        Runnable emptyRunnable = new Runnable() {
622            @Override
623            public void run() {
624            }
625        };
626        Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
627            @Override
628            public boolean apply(AccessibilityEvent t) {
629                if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
630                    return packageName == null || packageName.equals(t.getPackageName());
631                }
632                return false;
633            }
634        };
635        try {
636            getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent(
637                    emptyRunnable, checkWindowUpdate, timeout);
638        } catch (TimeoutException e) {
639            return false;
640        } catch (Exception e) {
641            Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e);
642            return false;
643        }
644        return true;
645    }
646}
647