blob: 7afc4854348c3415d205ffbf498281f407af424b [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.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Point;
import android.hardware.display.DisplayManagerGlobal;
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.KeyEvent;
import android.view.Surface;
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.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* UiDevice provides access to state information about the device.
* You can also use this class to simulate user actions on the device,
* such as pressing the d-pad or pressing the Home and Menu buttons.
* @since API Level 16
*/
public class UiDevice {
private static final String LOG_TAG = UiDevice.class.getSimpleName();
private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
// Sometimes HOME and BACK key presses will generate no events if already on
// home page or there is nothing to go back to, Set low timeouts.
private static final long KEY_PRESS_EVENT_TIMEOUT = 1 * 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;
}
/**
* Retrieves a singleton instance of UiDevice
*
* @return UiDevice instance
* @since API Level 16
*/
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 a Point containing the display size in dp
* @hide
*/
public Point getDisplaySizeDp() {
Display display = 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;
}
/**
* Retrieves the product name of the device.
*
* This method provides information on what type of device the test is running on. This value is
* the same as returned by invoking #adb shell getprop ro.product.name.
*
* @return product name of the device
* @since API Level 17
*/
public String getProductName() {
return Build.PRODUCT;
}
/**
* Retrieves the text from the last UI traversal event received.
*
* You can use this method to read the contents in a WebView container
* because the accessibility framework fires events
* as each text is highlighted. You can write a test to perform
* directional arrow presses to focus on different elements inside a WebView,
* and call this method to get the text from each traversed element.
* If you are testing a view container that can return a reference to a
* Document Object Model (DOM) object, your test should use the view's
* DOM instead.
*
* @return text of the last traversal event, else return an empty string
* @since API Level 16
*/
public String getLastTraversedText() {
return mUiAutomationBridge.getQueryController().getLastTraversedText();
}
/**
* Clears the text from the last UI traversal event.
* See {@link #getLastTraversedText()}.
* @since API Level 16
*/
public void clearLastTraversedText() {
mUiAutomationBridge.getQueryController().clearLastTraversedText();
}
/**
* Simulates a short press on the MENU button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressMenu() {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_MENU, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the BACK button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressBack() {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_BACK, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the HOME button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressHome() {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKeyAndWaitForEvent(
KeyEvent.KEYCODE_HOME, 0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
KEY_PRESS_EVENT_TIMEOUT);
}
/**
* Simulates a short press on the SEARCH button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressSearch() {
return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
}
/**
* Simulates a short press on the CENTER button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDPadCenter() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
}
/**
* Simulates a short press on the DOWN button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDPadDown() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
}
/**
* Simulates a short press on the UP button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDPadUp() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
}
/**
* Simulates a short press on the LEFT button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDPadLeft() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
}
/**
* Simulates a short press on the RIGHT button.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDPadRight() {
return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
}
/**
* Simulates a short press on the DELETE key.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressDelete() {
return pressKeyCode(KeyEvent.KEYCODE_DEL);
}
/**
* Simulates a short press on the ENTER key.
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressEnter() {
return pressKeyCode(KeyEvent.KEYCODE_ENTER);
}
/**
* Simulates a short press using a key code.
*
* See {@link KeyEvent}
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressKeyCode(int keyCode) {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0);
}
/**
* Simulates a short press using a key code.
*
* See {@link KeyEvent}.
* @param keyCode the key code of the event.
* @param metaState an integer in which each bit set to 1 represents a pressed meta key
* @return true if successful, else return false
* @since API Level 16
*/
public boolean pressKeyCode(int keyCode, int metaState) {
waitForIdle();
return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState);
}
/**
* Simulates a short press on the Recent Apps button.
*
* @return true if successful, else return false
* @throws RemoteException
* @since API Level 16
*/
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 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
* @since API Level 16
*/
public int getDisplayWidth() {
Display display = getDefaultDisplay();
Point p = new Point();
display.getSize(p);
return p.x;
}
/**
* 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
* @since API Level 16
*/
public int getDisplayHeight() {
Display display = getDefaultDisplay();
Point p = new Point();
display.getSize(p);
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
* @since API Level 16
*/
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
* @since API Level 16
*/
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
* @since API Level 16
*/
public boolean swipe(Point[] segments, int segmentSteps) {
return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
}
/**
* Waits for the current application to idle.
* Default wait timeout is 10 seconds
* @since API Level 16
*/
public void waitForIdle() {
waitForIdle(DEFAULT_TIMEOUT_MILLIS);
}
/**
* Waits for the current application to idle.
* @param timeout in milliseconds
* @since API Level 16
*/
public void waitForIdle(long timeout) {
mUiAutomationBridge.waitForIdle(timeout);
}
/**
* Retrieves the last activity to report accessibility events.
* @deprecated The results returned should be considered unreliable
* @return String name of activity
* @since API Level 16
*/
@Deprecated
public String getCurrentActivityName() {
return mUiAutomationBridge.getQueryController().getCurrentActivityName();
}
/**
* Retrieves the name of the last package to report accessibility events.
* @return String name of package
* @since API Level 16
*/
public String getCurrentPackageName() {
return mUiAutomationBridge.getQueryController().getCurrentPackageName();
}
/**
* Registers a {@link UiWatcher} to run automatically when the testing framework is unable to
* find a match using a {@link UiSelector}. See {@link #runWatchers()}
*
* @param name to register the UiWatcher
* @param watcher {@link UiWatcher}
* @since API Level 16
*/
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 UiWatcher}.
*
* See {@link #registerWatcher(String, UiWatcher)}
* @param name used to register the UiWatcher
* @since API Level 16
*/
public void removeWatcher(String name) {
if (mInWatcherContext) {
throw new IllegalStateException("Cannot remove a watcher from within another");
}
mWatchers.remove(name);
}
/**
* This method forces all registered watchers to run.
* See {@link #registerWatcher(String, UiWatcher)}
* @since API Level 16
*/
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;
}
}
}
}
/**
* Resets a {@link UiWatcher} that has been triggered.
* If a UiWatcher runs and its {@link UiWatcher#checkForCondition()} call
* returned <code>true</code>, then the UiWatcher is considered triggered.
* See {@link #registerWatcher(String, UiWatcher)}
* @since API Level 16
*/
public void resetWatcherTriggers() {
mWatchersTriggers.clear();
}
/**
* Checks if a specific registered {@link UiWatcher} has triggered.
* See {@link #registerWatcher(String, UiWatcher)}. If a UiWatcher runs and its
* {@link UiWatcher#checkForCondition()} call returned <code>true</code>, then
* the UiWatcher is considered triggered. 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.
*
* @param watcherName
* @return true if triggered else false
* @since API Level 16
*/
public boolean hasWatcherTriggered(String watcherName) {
return mWatchersTriggers.contains(watcherName);
}
/**
* Checks if any registered {@link UiWatcher} have triggered.
*
* See {@link #registerWatcher(String, UiWatcher)}
* See {@link #hasWatcherTriggered(String)}
* @since API Level 16
*/
public boolean hasAnyWatcherTriggered() {
return mWatchersTriggers.size() > 0;
}
/**
* Used internally by this class to set a {@link UiWatcher} state as triggered.
* @param watcherName
*/
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
* @since API Level 17
*/
public boolean isNaturalOrientation() {
Display display = getDefaultDisplay();
return display.getRotation() == Surface.ROTATION_0 ||
display.getRotation() == Surface.ROTATION_180;
}
/**
* Returns the current rotation of the display, as defined in {@link Surface}
* @since API Level 17
*/
public int getDisplayRotation() {
return getDefaultDisplay().getRotation();
}
/**
* Disables the sensors and freezes the device rotation at its
* current rotation state.
* @throws RemoteException
* @since API Level 16
*/
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. During a test execution, it is best to
* keep the device frozen in a specific orientation until the test case execution has completed.
* @throws RemoteException
*/
public void unfreezeRotation() throws RemoteException {
getAutomatorBridge().getInteractionController().unfreezeRotation();
}
/**
* Simulates orienting the device to the left and also freezes rotation
* by disabling the sensors.
*
* If you want to un-freeze the rotation and re-enable the sensors
* see {@link #unfreezeRotation()}.
* @throws RemoteException
* @since API Level 17
*/
public void setOrientationLeft() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationLeft();
}
/**
* Simulates orienting the device to the right and also freezes rotation
* by disabling the sensors.
*
* If you want to un-freeze the rotation and re-enable the sensors
* see {@link #unfreezeRotation()}.
* @throws RemoteException
* @since API Level 17
*/
public void setOrientationRight() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationRight();
}
/**
* Simulates orienting the device into its natural orientation and also freezes rotation
* by disabling the sensors.
*
* If you want to un-freeze the rotation and re-enable the sensors
* see {@link #unfreezeRotation()}.
* @throws RemoteException
* @since API Level 17
*/
public void setOrientationNatural() throws RemoteException {
getAutomatorBridge().getInteractionController().setRotationNatural();
}
/**
* This method simulates pressing 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
* @since API Level 16
*/
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
* @since API Level 16
*/
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
* @since API Level 16
*/
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
* @since API Level 16
*/
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 the window is specified, but the current window
* does not have the same package name, the function returns immediately.
*
* @param packageName the specified window package name (can be <code>null</code>).
* If <code>null</code>, a window update from any front-end window will end the wait
* @param timeout the timeout for the wait
*
* @return true if a window update occurred, false if timeout has elapsed or if the current
* window does not have the specified package name
* @since API Level 16
*/
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;
}
private static Display getDefaultDisplay() {
return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
}
/**
* @return the current display rotation in degrees
*/
private static float getDegreesForRotation(int value) {
switch (value) {
case Surface.ROTATION_90:
return 360f - 90f;
case Surface.ROTATION_180:
return 360f - 180f;
case Surface.ROTATION_270:
return 360f - 270f;
}
return 0f;
}
/**
* Take a screenshot of current window and store it as PNG
*
* Default scale of 1.0f (original size) and 90% quality is used
* The screenshot is adjusted per screen rotation
*
* @param storePath where the PNG should be written to
* @return true if screen shot is created successfully, false otherwise
* @since API Level 17
*/
public boolean takeScreenshot(File storePath) {
return takeScreenshot(storePath, 1.0f, 90);
}
/**
* Take a screenshot of current window and store it as PNG
*
* The screenshot is adjusted per screen rotation
*
* @param storePath where the PNG should be written to
* @param scale scale the screenshot down if needed; 1.0f for original size
* @param quality quality of the PNG compression; range: 0-100
* @return true if screen shot is created successfully, false otherwise
* @since API Level 17
*/
public boolean takeScreenshot(File storePath, float scale, int quality) {
// This is from com.android.systemui.screenshot.GlobalScreenshot#takeScreenshot
// We need to orient the screenshot correctly (and the Surface api seems to take screenshots
// only in the natural orientation of the device :!)
DisplayMetrics displayMetrics = new DisplayMetrics();
Display display = getDefaultDisplay();
display.getRealMetrics(displayMetrics);
float[] dims = {displayMetrics.widthPixels, displayMetrics.heightPixels};
float degrees = getDegreesForRotation(display.getRotation());
boolean requiresRotation = (degrees > 0);
Matrix matrix = new Matrix();
matrix.reset();
if (scale != 1.0f) {
matrix.setScale(scale, scale);
}
if (requiresRotation) {
// Get the dimensions of the device in its native orientation
matrix.preRotate(-degrees);
}
matrix.mapPoints(dims);
dims[0] = Math.abs(dims[0]);
dims[1] = Math.abs(dims[1]);
// Take the screenshot
Bitmap screenShot = Surface.screenshot((int) dims[0], (int) dims[1]);
if (screenShot == null) {
return false;
}
if (requiresRotation) {
// Rotate the screenshot to the current orientation
int width = displayMetrics.widthPixels;
int height = displayMetrics.heightPixels;
if (scale != 1.0f) {
width = Math.round(scale * width);
height = Math.round(scale * height);
}
Bitmap ss = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(ss);
c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
c.rotate(degrees);
c.translate(-dims[0] / 2, -dims[1] / 2);
c.drawBitmap(screenShot, 0, 0, null);
c.setBitmap(null);
screenShot = ss;
}
// Optimizations
screenShot.setHasAlpha(false);
try {
FileOutputStream fos = new FileOutputStream(storePath);
screenShot.compress(Bitmap.CompressFormat.PNG, quality, fos);
fos.flush();
fos.close();
} catch (IOException ioe) {
Log.e(LOG_TAG, "failed to save screen shot to file", ioe);
return false;
} finally {
screenShot.recycle();
}
return true;
}
}