blob: f827bb05c49dc9d1551695b10e96e1c47092c82b [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.uiautomator.core;
import android.content.Context;
import android.graphics.Point;
import android.os.Build;
import android.os.Environment;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.IWindowManager;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.WindowManagerImpl;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.Predicate;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* UiDevice provides access to device wide states. Also provides methods to simulate
* pressing hardware buttons such as DPad or the soft buttons such as Home and Menu.
*/
public class UiDevice {
private static final String LOG_TAG = UiDevice.class.getSimpleName();
private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
// store for registered UiWatchers
private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>();
private final List<String> mWatchersTriggers = new ArrayList<String>();
// remember if we're executing in the context of a UiWatcher
private boolean mInWatcherContext = false;
// provides access the {@link QueryController} and {@link InteractionController}
private final UiAutomatorBridge mUiAutomationBridge;
// reference to self
private static UiDevice mDevice;
private UiDevice() {
mUiAutomationBridge = new UiAutomatorBridge();
mDevice = this;
}
boolean isInWatcherContext() {
return mInWatcherContext;
}
/**
* Provides access the {@link QueryController} and {@link InteractionController}
* @return {@link UiAutomatorBridge}
*/
UiAutomatorBridge getAutomatorBridge() {
return mUiAutomationBridge;
}
/**
* Allow both the direct creation of a UiDevice and retrieving a existing
* instance of UiDevice. This helps tests and their libraries to have access
* to UiDevice with necessitating having to always pass copies of UiDevice
* instances around.
* @return UiDevice instance
*/
public static UiDevice getInstance() {
if (mDevice == null) {
mDevice = new UiDevice();
}
return mDevice;
}
/**
* Returns the display size in dp (device-independent pixel)
*
* The returned display size is adjusted per screen rotation
*
* @return
*/
public Point getDisplaySizeDp() {
Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
Point p = new Point();
display.getSize(p);
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
float dpx = p.x / metrics.density;
float dpy = p.y / metrics.density;
p.x = Math.round(dpx);
p.y = Math.round(dpy);
return p;
}
/**
* Returns the product name of the device
*
* This provides info on what type of device that the test is running on. However, for the
* purpose of adapting to different styles of UI, test should favor
* {@link UiDevice#getDisplaySizeDp()} over this method, and only use product name as a fallback
* mechanism
*/
public String getProductName() {
return Build.PRODUCT;
}
/**
* This method returns the text from the last UI traversal event received.
* This is helpful in WebView when the test performs directional arrow presses to focus
* on different elements inside the WebView. The accessibility fires events
* with every text highlighted. One can read the contents of a WebView control this way
* however slow slow and unreliable it is. When the view control used can return a
* reference to is Document Object Model, it is recommended then to use the view's
* DOM instead.
* @return text of the last traversal event else an empty string
*/
public String getLastTraversedText() {
return mUiAutomationBridge.getQueryController().getLastTraversedText();
}
/**
* Helper to clear the text saved from the last accessibility UI traversal event.
* See {@link #getLastTraversedText()}.
*/
public void clearLastTraversedText() {
mUiAutomationBridge.getQueryController().clearLastTraversedText();
}
/**
* Helper method to do a short press on MENU button
* @return true if successful else false
*/
public boolean pressMenu() {
return pressKeyCode(KeyEvent.KEYCODE_MENU);
}
/**
* Helper method to do a short press on BACK button
* @return true if successful else false
*/
public boolean pressBack() {
return pressKeyCode(KeyEvent.KEYCODE_BACK);
}
/**
* Helper method to do a short press on HOME button
* @return true if successful else false
*/
public boolean pressHome() {
return pressKeyCode(KeyEvent.KEYCODE_HOME);
}
/**
* Helper method to do a short press on SEARCH button
* @return true if successful else false
*/
public boolean pressSearch() {
return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
}
/**
* Helper method to do a short press on DOWN button
* @return true if successful else false
*/
public boolean pressDPadCenter() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
}
/**
* Helper method to do a short press on DOWN button
* @return true if successful else false
*/
public boolean pressDPadDown() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
}
/**
* Helper method to do a short press on UP button
* @return true if successful else false
*/
public boolean pressDPadUp() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
}
/**
* Helper method to do a short press on LEFT button
* @return true if successful else false
*/
public boolean pressDPadLeft() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
}
/**
* Helper method to do a short press on RIGTH button
* @return true if successful else false
*/
public boolean pressDPadRight() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
}
/**
* Helper method to do a short press on DELETE
* @return true if successful else false
*/
public boolean pressDelete() {
return pressKeyCode(KeyEvent.KEYCODE_DEL);
}
/**
* Helper method to do a short press on ENTER
* @return true if successful else false
*/
public boolean pressEnter() {
return pressKeyCode(KeyEvent.KEYCODE_ENTER);
}
/**
* Helper method to do a short press using a key code. See {@link KeyEvent}
* @return true if successful else false
*/
public boolean pressKeyCode(int keyCode) {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0);
}
/**
* Helper method to do a short press using a key code. See {@link KeyEvent}
* @param keyCode See {@link KeyEvent}
* @param metaState See {@link KeyEvent}
* @return true if successful else false
*/
public boolean pressKeyCode(int keyCode, int metaState) {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState);
}
/**
* Gets the width of the display, in pixels. The width and height details
* are reported based on the current orientation of the display.
* @return width in pixels or zero on failure
*/
public int getDisplayWidth() {
IWindowManager wm = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
Point p = new Point();
try {
wm.getDisplaySize(p);
} catch (RemoteException e) {
return 0;
}
return p.x;
}
/**
* Press recent apps soft key
* @return true if successful
* @throws RemoteException
*/
public boolean pressRecentApps() throws RemoteException {
waitForIdle();
final IStatusBarService statusBar = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
if (statusBar != null) {
statusBar.toggleRecentApps();
return true;
}
return false;
}
/**
* Gets the height of the display, in pixels. The size is adjusted based
* on the current orientation of the display.
* @return height in pixels or zero on failure
*/
public int getDisplayHeight() {
IWindowManager wm = IWindowManager.Stub.asInterface(
ServiceManager.getService(Context.WINDOW_SERVICE));
Point p = new Point();
try {
wm.getDisplaySize(p);
} catch (RemoteException e) {
return 0;
}
return p.y;
}
/**
* Perform a click at arbitrary coordinates specified by the user
*
* @param x coordinate
* @param y coordinate
* @return true if the click succeeded else false
*/
public boolean click(int x, int y) {
if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
return (false);
}
return getAutomatorBridge().getInteractionController().click(x, y);
}
/**
* Performs a swipe from one coordinate to another using the number of steps
* to determine smoothness and speed. Each step execution is throttled to 5ms
* per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
*
* @param startX
* @param startY
* @param endX
* @param endY
* @param steps is the number of move steps sent to the system
* @return false if the operation fails or the coordinates are invalid
*/
public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
return mUiAutomationBridge.getInteractionController()
.scrollSwipe(startX, startY, endX, endY, steps);
}
/**
* Performs a swipe between points in the Point array. Each step execution is throttled
* to 5ms per step. So for a 100 steps, the swipe will take about 1/2 second to complete
*
* @param segments is Point array containing at least one Point object
* @param segmentSteps steps to inject between two Points
* @return true on success
*/
public boolean swipe(Point[] segments, int segmentSteps) {
return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
}
public void waitForIdle() {
waitForIdle(DEFAULT_TIMEOUT_MILLIS);
}
public void waitForIdle(long time) {
mUiAutomationBridge.waitForIdle(time);
}
/**
* Last activity to report accessibility events
* @return String name of activity
*/
public String getCurrentActivityName() {
return mUiAutomationBridge.getQueryController().getCurrentActivityName();
}
/**
* Last package to report accessibility events
* @return String name of package
*/
public String getCurrentPackageName() {
return mUiAutomationBridge.getQueryController().getCurrentPackageName();
}
/**
* Registers a condition watcher to be called by the automation library only when a
* {@link UiObject} method call is in progress and is in retry waiting to match
* its UI element. Only during these conditions the watchers are invoked to check if
* there is something else unexpected on the screen that may be causing the match failure
* and retries. Under normal conditions when UiObject methods are immediately matching
* their UI element, watchers may never get to run. See {@link UiDevice#runWatchers()}
*
* @param name of watcher
* @param watcher {@link UiWatcher}
*/
public void registerWatcher(String name, UiWatcher watcher) {
if (mInWatcherContext) {
throw new IllegalStateException("Cannot register new watcher from within another");
}
mWatchers.put(name, watcher);
}
/**
* Removes a previously registered {@link #registerWatcher(String, UiWatcher)}.
*
* @param name of watcher used when <code>registerWatcher</code> was called.
* @throws UiAutomationException
*/
public void removeWatcher(String name) {
if (mInWatcherContext) {
throw new IllegalStateException("Cannot remove a watcher from within another");
}
mWatchers.remove(name);
}
/**
* See {@link #registerWatcher(String, UiWatcher)}. This forces all registered watchers
* to run.
*/
public void runWatchers() {
if (mInWatcherContext) {
return;
}
for (String watcherName : mWatchers.keySet()) {
UiWatcher watcher = mWatchers.get(watcherName);
if (watcher != null) {
try {
mInWatcherContext = true;
if (watcher.checkForCondition()) {
setWatcherTriggered(watcherName);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
} finally {
mInWatcherContext = false;
}
}
}
}
/**
* See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and
* returns true from its implementation of {@link UiWatcher#checkForCondition()} then
* it is considered triggered.
*/
public void resetWatcherTriggers() {
mWatchersTriggers.clear();
}
/**
* See {@link #registerWatcher(String, UiWatcher)}. If a watcher is run and
* returns true from its implementation of {@link UiWatcher#checkForCondition()} then
* it is considered triggered. This method can be used to check if a specific UiWatcher
* has been triggered during the test. This is helpful if a watcher is detecting errors
* from ANR or crash dialogs and the test needs to know if a UiWatcher has been triggered.
*/
public boolean hasWatcherTriggered(String watcherName) {
return mWatchersTriggers.contains(watcherName);
}
/**
* See {@link #registerWatcher(String, UiWatcher)} and {@link #hasWatcherTriggered(String)}
*/
public boolean hasAnyWatcherTriggered() {
return mWatchersTriggers.size() > 0;
}
private void setWatcherTriggered(String watcherName) {
if (!hasWatcherTriggered(watcherName)) {
mWatchersTriggers.add(watcherName);
}
}
/**
* Check if the device is in its natural orientation. This is determined by checking if the
* orientation is at 0 or 180 degrees.
* @return true if it is in natural orientation
*/
public boolean isNaturalOrientation() {
Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
return display.getRotation() == Surface.ROTATION_0 ||
display.getRotation() == Surface.ROTATION_180;
}
/**
* Disables the sensors and freezes the device rotation at its
* current rotation state.
* @throws RemoteException
*/
public void freezeRotation() throws RemoteException {
getAutomatorBridge().getInteractionController().freezeRotation();
}
/**
* Re-enables the sensors and un-freezes the device rotation allowing its contents
* to rotate with the device physical rotation. Note that by un-freezing the rotation,
* the screen contents may suddenly rotate depending on the current physical position
* of the test device. During a test execution, it is best to keep the device frozen
* in a specific orientation until the test case execution is completed.
* @throws RemoteException
*/
public void unfreezeRotation() throws RemoteException {
getAutomatorBridge().getInteractionController().unfreezeRotation();
}
/**
* Orients the device to the left and also freezes rotation in that
* orientation by disabling the sensors. If you want to un-freeze the rotation
* and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing
* so may cause the screen contents to get re-oriented depending on the current
* physical position of the test device.
* @throws RemoteException
*/
public void setOrientationLeft() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationLeft();
}
/**
* Orients the device to the right and also freezes rotation in that
* orientation by disabling the sensors. If you want to un-freeze the rotation
* and re-enable the sensors see {@link #unfreezeRotation()}. Note that doing
* so may cause the screen contents to get re-oriented depending on the current
* physical position of the test device.
* @throws RemoteException
*/
public void setOrientationRight() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationRight();
}
/**
* Rotates right and also freezes rotation in that orientation by
* disabling the sensors. If you want to un-freeze the rotation
* and re-enable the sensors see {@link #unfreezeRotation()}. Note
* that doing so may cause the screen contents to rotate
* depending on the current physical position of the test device.
* @throws RemoteException
*/
public void setOrientationNatural() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationNatural();
}
/**
* This method simply presses the power button if the screen is OFF else
* it does nothing if the screen is already ON. If the screen was OFF and
* it just got turned ON, this method will insert a 500ms delay to allow
* the device time to wake up and accept input.
* @throws RemoteException
*/
public void wakeUp() throws RemoteException {
if(getAutomatorBridge().getInteractionController().wakeDevice()) {
// sync delay to allow the window manager to start accepting input
// after the device is awakened.
SystemClock.sleep(500);
}
}
/**
* Checks the power manager if the screen is ON
* @return true if the screen is ON else false
* @throws RemoteException
*/
public boolean isScreenOn() throws RemoteException {
return getAutomatorBridge().getInteractionController().isScreenOn();
}
/**
* This method simply presses the power button if the screen is ON else
* it does nothing if the screen is already OFF.
* @throws RemoteException
*/
public void sleep() throws RemoteException {
getAutomatorBridge().getInteractionController().sleepDevice();
}
/**
* Helper method used for debugging to dump the current window's layout hierarchy.
* The file root location is /data/local/tmp
*
* @param fileName
*/
public void dumpWindowHierarchy(String fileName) {
AccessibilityNodeInfo root =
getAutomatorBridge().getQueryController().getAccessibilityRootNode();
if(root != null) {
AccessibilityNodeInfoDumper.dumpWindowToFile(
root, new File(new File(Environment.getDataDirectory(),
"local/tmp"), fileName));
}
}
/**
* Waits for a window content update event to occur
*
* if a package name for window is specified, but current window is not with the same package
* name, the function will return immediately
*
* @param packageName the specified window package name; maybe <code>null</code>, and a window
* update from any frontend window will end the wait
* @param timeout the timeout for the wait
*
* @return true if a window update occured, false if timeout has reached or current window is
* not the specified package name
*/
public boolean waitForWindowUpdate(final String packageName, long timeout) {
if (packageName != null) {
if (!packageName.equals(getCurrentPackageName())) {
return false;
}
}
Runnable emptyRunnable = new Runnable() {
@Override
public void run() {
}
};
Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
@Override
public boolean apply(AccessibilityEvent t) {
if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
return packageName == null || packageName.equals(t.getPackageName());
}
return false;
}
};
try {
getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent(
emptyRunnable, checkWindowUpdate, timeout);
} catch (TimeoutException e) {
return false;
} catch (Exception e) {
Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e);
return false;
}
return true;
}
}