summaryrefslogtreecommitdiffstats
path: root/cmds/uiautomator/library/core-src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'cmds/uiautomator/library/core-src/com/android')
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java250
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java52
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java224
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java795
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java521
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java285
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java143
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java145
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java851
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java1083
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java48
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java665
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java1022
-rw-r--r--cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java48
14 files changed, 6132 insertions, 0 deletions
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
new file mode 100644
index 0000000..63c51e8
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
@@ -0,0 +1,250 @@
+/*
+ * 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.os.Environment;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Xml;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+
+/**
+ *
+ * @hide
+ */
+public class AccessibilityNodeInfoDumper {
+
+ private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
+ private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
+ android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
+ android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
+ };
+
+ /**
+ * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
+ * and generates an xml dump into the /data/local/window_dump.xml
+ * @param root The root accessibility node.
+ * @param rotation The rotaion of current display
+ * @param width The pixel width of current display
+ * @param height The pixel height of current display
+ */
+ public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
+ int width, int height) {
+ File baseDir = new File(Environment.getDataDirectory(), "local");
+ if (!baseDir.exists()) {
+ baseDir.mkdir();
+ baseDir.setExecutable(true, false);
+ baseDir.setWritable(true, false);
+ baseDir.setReadable(true, false);
+ }
+ dumpWindowToFile(root,
+ new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
+ rotation, width, height);
+ }
+
+ /**
+ * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
+ * and generates an xml dump to the location specified by <code>dumpFile</code>
+ * @param root The root accessibility node.
+ * @param dumpFile The file to dump to.
+ * @param rotation The rotaion of current display
+ * @param width The pixel width of current display
+ * @param height The pixel height of current display
+ */
+ public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
+ int width, int height) {
+ if (root == null) {
+ return;
+ }
+ final long startTime = SystemClock.uptimeMillis();
+ try {
+ FileWriter writer = new FileWriter(dumpFile);
+ XmlSerializer serializer = Xml.newSerializer();
+ StringWriter stringWriter = new StringWriter();
+ serializer.setOutput(stringWriter);
+ serializer.startDocument("UTF-8", true);
+ serializer.startTag("", "hierarchy");
+ serializer.attribute("", "rotation", Integer.toString(rotation));
+ dumpNodeRec(root, serializer, 0, width, height);
+ serializer.endTag("", "hierarchy");
+ serializer.endDocument();
+ writer.write(stringWriter.toString());
+ writer.close();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "failed to dump window to file", e);
+ }
+ final long endTime = SystemClock.uptimeMillis();
+ Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
+ }
+
+ private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
+ int width, int height) throws IOException {
+ serializer.startTag("", "node");
+ if (!nafExcludedClass(node) && !nafCheck(node))
+ serializer.attribute("", "NAF", Boolean.toString(true));
+ serializer.attribute("", "index", Integer.toString(index));
+ serializer.attribute("", "text", safeCharSeqToString(node.getText()));
+ serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
+ serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
+ serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
+ serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
+ serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
+ serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
+ serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
+ serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
+ serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
+ serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
+ serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
+ serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
+ serializer.attribute("", "password", Boolean.toString(node.isPassword()));
+ serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
+ serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+ node, width, height).toShortString());
+ int count = node.getChildCount();
+ for (int i = 0; i < count; i++) {
+ AccessibilityNodeInfo child = node.getChild(i);
+ if (child != null) {
+ if (child.isVisibleToUser()) {
+ dumpNodeRec(child, serializer, i, width, height);
+ child.recycle();
+ } else {
+ Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
+ }
+ } else {
+ Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
+ i, count, node.toString()));
+ }
+ }
+ serializer.endTag("", "node");
+ }
+
+ /**
+ * The list of classes to exclude my not be complete. We're attempting to
+ * only reduce noise from standard layout classes that may be falsely
+ * configured to accept clicks and are also enabled.
+ *
+ * @param node
+ * @return true if node is excluded.
+ */
+ private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
+ String className = safeCharSeqToString(node.getClassName());
+ for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
+ if(className.endsWith(excludedClassName))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * We're looking for UI controls that are enabled, clickable but have no
+ * text nor content-description. Such controls configuration indicate an
+ * interactive control is present in the UI and is most likely not
+ * accessibility friendly. We refer to such controls here as NAF controls
+ * (Not Accessibility Friendly)
+ *
+ * @param node
+ * @return false if a node fails the check, true if all is OK
+ */
+ private static boolean nafCheck(AccessibilityNodeInfo node) {
+ boolean isNaf = node.isClickable() && node.isEnabled()
+ && safeCharSeqToString(node.getContentDescription()).isEmpty()
+ && safeCharSeqToString(node.getText()).isEmpty();
+
+ if (!isNaf)
+ return true;
+
+ // check children since sometimes the containing element is clickable
+ // and NAF but a child's text or description is available. Will assume
+ // such layout as fine.
+ return childNafCheck(node);
+ }
+
+ /**
+ * This should be used when it's already determined that the node is NAF and
+ * a further check of its children is in order. A node maybe a container
+ * such as LinerLayout and may be set to be clickable but have no text or
+ * content description but it is counting on one of its children to fulfill
+ * the requirement for being accessibility friendly by having one or more of
+ * its children fill the text or content-description. Such a combination is
+ * considered by this dumper as acceptable for accessibility.
+ *
+ * @param node
+ * @return false if node fails the check.
+ */
+ private static boolean childNafCheck(AccessibilityNodeInfo node) {
+ int childCount = node.getChildCount();
+ for (int x = 0; x < childCount; x++) {
+ AccessibilityNodeInfo childNode = node.getChild(x);
+
+ if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
+ || !safeCharSeqToString(childNode.getText()).isEmpty())
+ return true;
+
+ if (childNafCheck(childNode))
+ return true;
+ }
+ return false;
+ }
+
+ private static String safeCharSeqToString(CharSequence cs) {
+ if (cs == null)
+ return "";
+ else {
+ return stripInvalidXMLChars(cs);
+ }
+ }
+
+ private static String stripInvalidXMLChars(CharSequence cs) {
+ StringBuffer ret = new StringBuffer();
+ char ch;
+ /* http://www.w3.org/TR/xml11/#charsets
+ [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
+ [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
+ [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
+ [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
+ [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
+ [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
+ [#x10FFFE-#x10FFFF].
+ */
+ for (int i = 0; i < cs.length(); i++) {
+ ch = cs.charAt(i);
+
+ if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
+ (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
+ (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
+ (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
+ (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
+ (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
+ (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
+ (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
+ (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
+ (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
+ (ch >= 0x10FFFE && ch <= 0x10FFFF))
+ ret.append(".");
+ else
+ ret.append(ch);
+ }
+ return ret.toString();
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
new file mode 100644
index 0000000..54835e3
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
@@ -0,0 +1,52 @@
+/*
+ * 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.graphics.Rect;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * This class contains static helper methods to work with
+ * {@link AccessibilityNodeInfo}
+ */
+class AccessibilityNodeInfoHelper {
+
+ /**
+ * Returns the node's bounds clipped to the size of the display
+ *
+ * @param node
+ * @param width pixel width of the display
+ * @param height pixel height of the display
+ * @return null if node is null, else a Rect containing visible bounds
+ */
+ static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
+ if (node == null) {
+ return null;
+ }
+ // targeted node's bounds
+ Rect nodeRect = new Rect();
+ node.getBoundsInScreen(nodeRect);
+
+ Rect displayRect = new Rect();
+ displayRect.top = 0;
+ displayRect.left = 0;
+ displayRect.right = width;
+ displayRect.bottom = height;
+
+ nodeRect.intersect(displayRect);
+ return nodeRect;
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java
new file mode 100644
index 0000000..249f404
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2013 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;
+
+/**
+ * Allows you to set key parameters for running uiautomator tests. The new
+ * settings take effect immediately and can be changed any time during a test run.
+ *
+ * To modify parameters using Configurator, first obtain an instance by calling
+ * {@link #getInstance()}. As a best practice, make sure you always save
+ * the original value of any parameter that you are modifying. After running your
+ * tests with the modified parameters, make sure to also restore
+ * the original parameter values, otherwise this will impact other tests cases.
+ * @since API Level 18
+ */
+public final class Configurator {
+ private long mWaitForIdleTimeout = 10 * 1000;
+ private long mWaitForSelector = 10 * 1000;
+ private long mWaitForActionAcknowledgment = 3 * 1000;
+
+ // The events for a scroll typically complete even before touchUp occurs.
+ // This short timeout to make sure we get the very last in cases where the above isn't true.
+ private long mScrollEventWaitTimeout = 200; // ms
+
+ // Default is inject as fast as we can
+ private long mKeyInjectionDelay = 0; // ms
+
+ // reference to self
+ private static Configurator sConfigurator;
+
+ private Configurator() {
+ /* hide constructor */
+ }
+
+ /**
+ * Retrieves a singleton instance of Configurator.
+ *
+ * @return Configurator instance
+ * @since API Level 18
+ */
+ public static Configurator getInstance() {
+ if (sConfigurator == null) {
+ sConfigurator = new Configurator();
+ }
+ return sConfigurator;
+ }
+
+ /**
+ * Sets the timeout for waiting for the user interface to go into an idle
+ * state before starting a uiautomator action.
+ *
+ * By default, all core uiautomator objects except {@link UiDevice} will perform
+ * this wait before starting to search for the widget specified by the
+ * object's {@link UiSelector}. Once the idle state is detected or the
+ * timeout elapses (whichever occurs first), the object will start to wait
+ * for the selector to find a match.
+ * See {@link #setWaitForSelectorTimeout(long)}
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setWaitForIdleTimeout(long timeout) {
+ mWaitForIdleTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the current timeout used for waiting for the user interface to go
+ * into an idle state.
+ *
+ * By default, all core uiautomator objects except {@link UiDevice} will perform
+ * this wait before starting to search for the widget specified by the
+ * object's {@link UiSelector}. Once the idle state is detected or the
+ * timeout elapses (whichever occurs first), the object will start to wait
+ * for the selector to find a match.
+ * See {@link #setWaitForSelectorTimeout(long)}
+ *
+ * @return Current timeout value in milliseconds
+ * @since API Level 18
+ */
+ public long getWaitForIdleTimeout() {
+ return mWaitForIdleTimeout;
+ }
+
+ /**
+ * Sets the timeout for waiting for a widget to become visible in the user
+ * interface so that it can be matched by a selector.
+ *
+ * Because user interface content is dynamic, sometimes a widget may not
+ * be visible immediately and won't be detected by a selector. This timeout
+ * allows the uiautomator framework to wait for a match to be found, up until
+ * the timeout elapses.
+ *
+ * @param timeout Timeout value in milliseconds.
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setWaitForSelectorTimeout(long timeout) {
+ mWaitForSelector = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the current timeout for waiting for a widget to become visible in
+ * the user interface so that it can be matched by a selector.
+ *
+ * Because user interface content is dynamic, sometimes a widget may not
+ * be visible immediately and won't be detected by a selector. This timeout
+ * allows the uiautomator framework to wait for a match to be found, up until
+ * the timeout elapses.
+ *
+ * @return Current timeout value in milliseconds
+ * @since API Level 18
+ */
+ public long getWaitForSelectorTimeout() {
+ return mWaitForSelector;
+ }
+
+ /**
+ * Sets the timeout for waiting for an acknowledgement of an
+ * uiautomtor scroll swipe action.
+ *
+ * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>,
+ * corresponding to the scroll action, that lets the framework determine if
+ * the scroll action was successful. Generally, this timeout should not be modified.
+ * See {@link UiScrollable}
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setScrollAcknowledgmentTimeout(long timeout) {
+ mScrollEventWaitTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the timeout for waiting for an acknowledgement of an
+ * uiautomtor scroll swipe action.
+ *
+ * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>,
+ * corresponding to the scroll action, that lets the framework determine if
+ * the scroll action was successful. Generally, this timeout should not be modified.
+ * See {@link UiScrollable}
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getScrollAcknowledgmentTimeout() {
+ return mScrollEventWaitTimeout;
+ }
+
+ /**
+ * Sets the timeout for waiting for an acknowledgment of generic uiautomator
+ * actions, such as clicks, text setting, and menu presses.
+ *
+ * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>,
+ * corresponding to an action, that lets the framework determine if the
+ * action was successful. Generally, this timeout should not be modified.
+ * See {@link UiObject}
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setActionAcknowledgmentTimeout(long timeout) {
+ mWaitForActionAcknowledgment = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the current timeout for waiting for an acknowledgment of generic
+ * uiautomator actions, such as clicks, text setting, and menu presses.
+ *
+ * The acknowledgment is an <a href="http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html">AccessibilityEvent</a>,
+ * corresponding to an action, that lets the framework determine if the
+ * action was successful. Generally, this timeout should not be modified.
+ * See {@link UiObject}
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getActionAcknowledgmentTimeout() {
+ return mWaitForActionAcknowledgment;
+ }
+
+ /**
+ * Sets a delay between key presses when injecting text input.
+ * See {@link UiObject#setText(String)}
+ *
+ * @param delay Delay value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setKeyInjectionDelay(long delay) {
+ mKeyInjectionDelay = delay;
+ return this;
+ }
+
+ /**
+ * Gets the current delay between key presses when injecting text input.
+ * See {@link UiObject#setText(String)}
+ *
+ * @return current delay in milliseconds
+ * @since API Level 18
+ */
+ public long getKeyInjectionDelay() {
+ return mKeyInjectionDelay;
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
new file mode 100644
index 0000000..73e46f1
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
@@ -0,0 +1,795 @@
+/*
+ * 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.accessibilityservice.AccessibilityService;
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.graphics.Point;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.util.Predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * The InteractionProvider is responsible for injecting user events such as touch events
+ * (includes swipes) and text key events into the system. To do so, all it needs to know about
+ * are coordinates of the touch events and text for the text input events.
+ * The InteractionController performs no synchronization. It will fire touch and text input events
+ * as fast as it receives them. All idle synchronization is performed prior to querying the
+ * hierarchy. See {@link QueryController}
+ */
+class InteractionController {
+
+ private static final String LOG_TAG = InteractionController.class.getSimpleName();
+
+ private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+ private final KeyCharacterMap mKeyCharacterMap =
+ KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+
+ private final UiAutomatorBridge mUiAutomatorBridge;
+
+ private static final long REGULAR_CLICK_LENGTH = 100;
+
+ private long mDownTime;
+
+ // Inserted after each motion event injection.
+ private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+
+ public InteractionController(UiAutomatorBridge bridge) {
+ mUiAutomatorBridge = bridge;
+ }
+
+ /**
+ * Predicate for waiting for any of the events specified in the mask
+ */
+ class WaitForAnyEventPredicate implements AccessibilityEventFilter {
+ int mMask;
+ WaitForAnyEventPredicate(int mask) {
+ mMask = mask;
+ }
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ return true;
+ }
+
+ // no match yet
+ return false;
+ }
+ }
+
+ /**
+ * Predicate for waiting for all the events specified in the mask and populating
+ * a ctor passed list with matching events. User of this Predicate must recycle
+ * all populated events in the events list.
+ */
+ class EventCollectingPredicate implements AccessibilityEventFilter {
+ int mMask;
+ List<AccessibilityEvent> mEventsList;
+
+ EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
+ mMask = mask;
+ mEventsList = events;
+ }
+
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ // For the events you need, always store a copy when returning false from
+ // predicates since the original will automatically be recycled after the call.
+ mEventsList.add(AccessibilityEvent.obtain(t));
+ }
+
+ // get more
+ return false;
+ }
+ }
+
+ /**
+ * Predicate for waiting for every event specified in the mask to be matched at least once
+ */
+ class WaitForAllEventPredicate implements AccessibilityEventFilter {
+ int mMask;
+ WaitForAllEventPredicate(int mask) {
+ mMask = mask;
+ }
+
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ // remove from mask since this condition is satisfied
+ mMask &= ~t.getEventType();
+
+ // Since we're waiting for all events to be matched at least once
+ if (mMask != 0)
+ return false;
+
+ // all matched
+ return true;
+ }
+
+ // no match yet
+ return false;
+ }
+ }
+
+ /**
+ * Helper used by methods to perform actions and wait for any accessibility events and return
+ * predicated on predefined filter.
+ *
+ * @param command
+ * @param filter
+ * @param timeout
+ * @return
+ */
+ private AccessibilityEvent runAndWaitForEvents(Runnable command,
+ AccessibilityEventFilter filter, long timeout) {
+
+ try {
+ return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter,
+ timeout);
+ } catch (TimeoutException e) {
+ Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
+ return null;
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
+ return null;
+ }
+ }
+
+ /**
+ * Send keys and blocks until the first specified accessibility event.
+ *
+ * Most key presses will cause some UI change to occur. If the device is busy, this will
+ * block until the device begins to process the key press at which point the call returns
+ * and normal wait for idle processing may begin. If no events are detected for the
+ * timeout period specified, the call will return anyway with false.
+ *
+ * @param keyCode
+ * @param metaState
+ * @param eventType
+ * @param timeout
+ * @return true if events is received, otherwise false.
+ */
+ public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
+ final int eventType, long timeout) {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ injectEventSync(upEvent);
+ }
+ }
+ };
+
+ return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
+ != null;
+ }
+
+ /**
+ * Clicks at coordinates without waiting for device idle. This may be used for operations
+ * that require stressing the target.
+ * @param x
+ * @param y
+ * @return true if the click executed successfully
+ */
+ public boolean clickNoSync(int x, int y) {
+ Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");
+
+ if (touchDown(x, y)) {
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ if (touchUp(x, y))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
+ * or TYPE_VIEW_SELECTED are received.
+ *
+ * @param x
+ * @param y
+ * @param timeout waiting for event
+ * @return true if events are received, else false if timeout.
+ */
+ public boolean clickAndSync(final int x, final int y, long timeout) {
+
+ String logString = String.format("clickAndSync(%d, %d)", x, y);
+ Log.d(LOG_TAG, logString);
+
+ return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate(
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
+ AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
+ }
+
+ /**
+ * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
+ * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
+ * no further waits will be performed and the function returns.
+ * @param x
+ * @param y
+ * @param timeout waiting for event
+ * @return true if both events occurred in the expected order
+ */
+ public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
+ String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y);
+ Log.d(LOG_TAG, logString);
+
+ return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate(
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null;
+ }
+
+ /**
+ * Returns a Runnable for use in {@link #runAndWaitForEvents(Runnable, Predicate, long) to
+ * perform a click.
+ *
+ * @param x coordinate
+ * @param y coordinate
+ * @return Runnable
+ */
+ private Runnable clickRunnable(final int x, final int y) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ if(touchDown(x, y)) {
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ touchUp(x, y);
+ }
+ }
+ };
+ }
+
+ /**
+ * Touches down for a long press at the specified coordinates.
+ *
+ * @param x
+ * @param y
+ * @return true if successful.
+ */
+ public boolean longTapNoSync(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
+ }
+
+ if (touchDown(x, y)) {
+ SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
+ if(touchUp(x, y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean touchDown(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
+ }
+ mDownTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ return injectEventSync(event);
+ }
+
+ private boolean touchUp(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
+ }
+ final long eventTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ mDownTime = 0;
+ return injectEventSync(event);
+ }
+
+ private boolean touchMove(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
+ }
+ final long eventTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ return injectEventSync(event);
+ }
+
+ /**
+ * Handle swipes in any direction where the result is a scroll event. This call blocks
+ * until the UI has fired a scroll event or timeout.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @return true if we are not at the beginning or end of the scrollable view.
+ */
+ public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
+ final int steps) {
+ Log.d(LOG_TAG, "scrollSwipe (" + downX + ", " + downY + ", " + upX + ", "
+ + upY + ", " + steps +")");
+
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ swipe(downX, downY, upX, upY, steps);
+ }
+ };
+
+ // Collect all accessibility events generated during the swipe command and get the
+ // last event
+ ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
+ runAndWaitForEvents(command,
+ new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
+ Configurator.getInstance().getScrollAcknowledgmentTimeout());
+
+ AccessibilityEvent event = getLastMatchingEvent(events,
+ AccessibilityEvent.TYPE_VIEW_SCROLLED);
+
+ if (event == null) {
+ // end of scroll since no new scroll events received
+ recycleAccessibilityEvents(events);
+ return false;
+ }
+
+ // AdapterViews have indices we can use to check for the beginning.
+ boolean foundEnd = false;
+ if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
+ foundEnd = event.getFromIndex() == 0 ||
+ (event.getItemCount() - 1) == event.getToIndex();
+ Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
+ } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
+ // Determine if we are scrolling vertically or horizontally.
+ if (downX == upX) {
+ // Vertical
+ foundEnd = event.getScrollY() == 0 ||
+ event.getScrollY() == event.getMaxScrollY();
+ Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
+ } else if (downY == upY) {
+ // Horizontal
+ foundEnd = event.getScrollX() == 0 ||
+ event.getScrollX() == event.getMaxScrollX();
+ Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
+ }
+ }
+ recycleAccessibilityEvents(events);
+ return !foundEnd;
+ }
+
+ private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
+ for (int x = events.size(); x > 0; x--) {
+ AccessibilityEvent event = events.get(x - 1);
+ if (event.getEventType() == type)
+ return event;
+ }
+ return null;
+ }
+
+ private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
+ for (AccessibilityEvent event : events)
+ event.recycle();
+ events.clear();
+ }
+
+ /**
+ * Handle swipes in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @return true if the swipe executed successfully
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
+ return swipe(downX, downY, upX, upY, steps, false /*drag*/);
+ }
+
+ /**
+ * Handle swipes/drags in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @param drag when true, the swipe becomes a drag swipe
+ * @return true if the swipe executed successfully
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
+ boolean ret = false;
+ int swipeSteps = steps;
+ double xStep = 0;
+ double yStep = 0;
+
+ // avoid a divide by zero
+ if(swipeSteps == 0)
+ swipeSteps = 1;
+
+ xStep = ((double)(upX - downX)) / swipeSteps;
+ yStep = ((double)(upY - downY)) / swipeSteps;
+
+ // first touch starts exactly at the point requested
+ ret = touchDown(downX, downY);
+ if (drag)
+ SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
+ for(int i = 1; i < swipeSteps; i++) {
+ ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
+ if(ret == false)
+ break;
+ // set some known constant delay between steps as without it this
+ // become completely dependent on the speed of the system and results
+ // may vary on different devices. This guarantees at minimum we have
+ // a preset delay.
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+ if (drag)
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ ret &= touchUp(upX, upY);
+ return(ret);
+ }
+
+ /**
+ * Performs a swipe between points in the Point array.
+ * @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) {
+ boolean ret = false;
+ int swipeSteps = segmentSteps;
+ double xStep = 0;
+ double yStep = 0;
+
+ // avoid a divide by zero
+ if(segmentSteps == 0)
+ segmentSteps = 1;
+
+ // must have some points
+ if(segments.length == 0)
+ return false;
+
+ // first touch starts exactly at the point requested
+ ret = touchDown(segments[0].x, segments[0].y);
+ for(int seg = 0; seg < segments.length; seg++) {
+ if(seg + 1 < segments.length) {
+
+ xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps;
+ yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps;
+
+ for(int i = 1; i < swipeSteps; i++) {
+ ret &= touchMove(segments[seg].x + (int)(xStep * i),
+ segments[seg].y + (int)(yStep * i));
+ if(ret == false)
+ break;
+ // set some known constant delay between steps as without it this
+ // become completely dependent on the speed of the system and results
+ // may vary on different devices. This guarantees at minimum we have
+ // a preset delay.
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+ }
+ }
+ ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y);
+ return(ret);
+ }
+
+
+ public boolean sendText(String text) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "sendText (" + text + ")");
+ }
+
+ KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
+
+ if (events != null) {
+ long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
+ for (KeyEvent event2 : events) {
+ // We have to change the time of an event before injecting it because
+ // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+ // time stamp and the system rejects too old events. Hence, it is
+ // possible for an event to become stale before it is injected if it
+ // takes too long to inject the preceding ones.
+ KeyEvent event = KeyEvent.changeTimeRepeat(event2,
+ SystemClock.uptimeMillis(), 0);
+ if (!injectEventSync(event)) {
+ return false;
+ }
+ SystemClock.sleep(keyDelay);
+ }
+ }
+ return true;
+ }
+
+ public boolean sendKey(int keyCode, int metaState) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
+ }
+
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if(injectEventSync(upEvent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Rotates right and also freezes rotation in that position 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 setRotationRight() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270);
+ }
+
+ /**
+ * Rotates left and also freezes rotation in that position 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 setRotationLeft() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90);
+ }
+
+ /**
+ * Rotates up and also freezes rotation in that position 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 setRotationNatural() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0);
+ }
+
+ /**
+ * Disables the sensors and freezes the device rotation at its
+ * current rotation state.
+ * @throws RemoteException
+ */
+ public void freezeRotation() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
+ }
+
+ /**
+ * Re-enables the sensors and un-freezes the device rotation
+ * allowing its contents to rotate with the device physical rotation.
+ * @throws RemoteException
+ */
+ public void unfreezeRotation() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE);
+ }
+
+ /**
+ * This method simply presses the power button if the screen is OFF else
+ * it does nothing if the screen is already ON.
+ * @return true if the device was asleep else false
+ * @throws RemoteException
+ */
+ public boolean wakeDevice() throws RemoteException {
+ if(!isScreenOn()) {
+ sendKey(KeyEvent.KEYCODE_POWER, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This method simply presses the power button if the screen is ON else
+ * it does nothing if the screen is already OFF.
+ * @return true if the device was awake else false
+ * @throws RemoteException
+ */
+ public boolean sleepDevice() throws RemoteException {
+ if(isScreenOn()) {
+ this.sendKey(KeyEvent.KEYCODE_POWER, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 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 mUiAutomatorBridge.isScreenOn();
+ }
+
+ private boolean injectEventSync(InputEvent event) {
+ return mUiAutomatorBridge.injectInputEvent(event, true);
+ }
+
+ private int getPointerAction(int motionEnvent, int index) {
+ return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ }
+
+ /**
+ * Performs a multi-touch gesture
+ *
+ * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
+ * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
+ * to specify the touch points along the path of a pointer, the caller is able to specify
+ * complex gestures like circles, irregular shapes etc, where each pointer may take a
+ * different path.
+ *
+ * To create a single point on a pointer's touch path
+ * <code>
+ * PointerCoords p = new PointerCoords();
+ * p.x = stepX;
+ * p.y = stepY;
+ * p.pressure = 1;
+ * p.size = 1;
+ * </code>
+ * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
+ * Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
+ * path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
+ * @return <code>true</code> if all points on all paths are injected successfully, <code>false
+ * </code>otherwise
+ * @since API Level 18
+ */
+ public boolean performMultiPointerGesture(PointerCoords[] ... touches) {
+ boolean ret = true;
+ if (touches.length < 2) {
+ throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
+ }
+
+ // Get the pointer with the max steps to inject.
+ int maxSteps = 0;
+ for (int x = 0; x < touches.length; x++)
+ maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps;
+
+ // specify the properties for each pointer as finger touch
+ PointerProperties[] properties = new PointerProperties[touches.length];
+ PointerCoords[] pointerCoords = new PointerCoords[touches.length];
+ for (int x = 0; x < touches.length; x++) {
+ PointerProperties prop = new PointerProperties();
+ prop.id = x;
+ prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
+ properties[x] = prop;
+
+ // for each pointer set the first coordinates for touch down
+ pointerCoords[x] = touches[x][0];
+ }
+
+ // Touch down all pointers
+ long downTime = SystemClock.uptimeMillis();
+ MotionEvent event;
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
+ properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ ret &= injectEventSync(event);
+
+ for (int x = 1; x < touches.length; x++) {
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
+ pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ ret &= injectEventSync(event);
+ }
+
+ // Move all pointers
+ for (int i = 1; i < maxSteps - 1; i++) {
+ // for each pointer
+ for (int x = 0; x < touches.length; x++) {
+ // check if it has coordinates to move
+ if (touches[x].length > i)
+ pointerCoords[x] = touches[x][i];
+ else
+ pointerCoords[x] = touches[x][touches[x].length - 1];
+ }
+
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
+ 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+
+ ret &= injectEventSync(event);
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+
+ // For each pointer get the last coordinates
+ for (int x = 0; x < touches.length; x++)
+ pointerCoords[x] = touches[x][touches[x].length - 1];
+
+ // touch up
+ for (int x = 1; x < touches.length; x++) {
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
+ pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ ret &= injectEventSync(event);
+ }
+
+ Log.i(LOG_TAG, "x " + pointerCoords[0].x);
+ // first to touch down is last up
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
+ properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ ret &= injectEventSync(event);
+ return ret;
+ }
+
+ /**
+ * Simulates a short press on the Recent Apps button.
+ *
+ * @return true if successful, else return false
+ * @since API Level 18
+ */
+ public boolean toggleRecentApps() {
+ return mUiAutomatorBridge.performGlobalAction(
+ AccessibilityService.GLOBAL_ACTION_RECENTS);
+ }
+
+ /**
+ * Opens the notification shade
+ *
+ * @return true if successful, else return false
+ * @since API Level 18
+ */
+ public boolean openNotification() {
+ return mUiAutomatorBridge.performGlobalAction(
+ AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
+ }
+
+ /**
+ * Opens the quick settings shade
+ *
+ * @return true if successful, else return false
+ * @since API Level 18
+ */
+ public boolean openQuickSettings() {
+ return mUiAutomatorBridge.performGlobalAction(
+ AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
new file mode 100644
index 0000000..6931528
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
@@ -0,0 +1,521 @@
+/*
+ * 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.app.UiAutomation.OnAccessibilityEventListener;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+
+/**
+ * The QueryController main purpose is to translate a {@link UiSelector} selectors to
+ * {@link AccessibilityNodeInfo}. This is all this controller does.
+ */
+class QueryController {
+
+ private static final String LOG_TAG = QueryController.class.getSimpleName();
+
+ private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
+ private static final boolean VERBOSE = Log.isLoggable(LOG_TAG, Log.VERBOSE);
+
+ private final UiAutomatorBridge mUiAutomatorBridge;
+
+ private final Object mLock = new Object();
+
+ private String mLastActivityName = null;
+
+ // During a pattern selector search, the recursive pattern search
+ // methods will track their counts and indexes here.
+ private int mPatternCounter = 0;
+ private int mPatternIndexer = 0;
+
+ // These help show each selector's search context as it relates to the previous sub selector
+ // matched. When a compound selector fails, it is hard to tell which part of it is failing.
+ // Seeing how a selector is being parsed and which sub selector failed within a long list
+ // of compound selectors is very helpful.
+ private int mLogIndent = 0;
+ private int mLogParentIndent = 0;
+
+ private String mLastTraversedText = "";
+
+ public QueryController(UiAutomatorBridge bridge) {
+ mUiAutomatorBridge = bridge;
+ bridge.setOnAccessibilityEventListener(new OnAccessibilityEventListener() {
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ switch(event.getEventType()) {
+ case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+ // don't trust event.getText(), check for nulls
+ if (event.getText() != null && event.getText().size() > 0) {
+ if(event.getText().get(0) != null)
+ mLastActivityName = event.getText().get(0).toString();
+ }
+ break;
+ case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
+ // don't trust event.getText(), check for nulls
+ if (event.getText() != null && event.getText().size() > 0)
+ if(event.getText().get(0) != null)
+ mLastTraversedText = event.getText().get(0).toString();
+ if (DEBUG)
+ Log.d(LOG_TAG, "Last text selection reported: " +
+ mLastTraversedText);
+ break;
+ }
+ mLock.notifyAll();
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the last text selection reported by accessibility
+ * event TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY. One way to cause
+ * this event is using a DPad arrows to focus on UI elements.
+ */
+ public String getLastTraversedText() {
+ mUiAutomatorBridge.waitForIdle();
+ synchronized (mLock) {
+ if (mLastTraversedText.length() > 0) {
+ return mLastTraversedText;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Clears the last text selection value saved from the TYPE_VIEW_TEXT_SELECTION_CHANGED
+ * event
+ */
+ public void clearLastTraversedText() {
+ mUiAutomatorBridge.waitForIdle();
+ synchronized (mLock) {
+ mLastTraversedText = "";
+ }
+ }
+
+ private void initializeNewSearch() {
+ mPatternCounter = 0;
+ mPatternIndexer = 0;
+ mLogIndent = 0;
+ mLogParentIndent = 0;
+ }
+
+ /**
+ * Counts the instances of the selector group. The selector must be in the following
+ * format: [container_selector, PATTERN=[INSTANCE=x, PATTERN=[the_pattern]]
+ * where the container_selector is used to find the containment region to search for patterns
+ * and the INSTANCE=x is the instance of the_pattern to return.
+ * @param selector
+ * @return number of pattern matches. Returns 0 for all other cases.
+ */
+ public int getPatternCount(UiSelector selector) {
+ findAccessibilityNodeInfo(selector, true /*counting*/);
+ return mPatternCounter;
+ }
+
+ /**
+ * Main search method for translating By selectors to AccessibilityInfoNodes
+ * @param selector
+ * @return AccessibilityNodeInfo
+ */
+ public AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector) {
+ return findAccessibilityNodeInfo(selector, false);
+ }
+
+ protected AccessibilityNodeInfo findAccessibilityNodeInfo(UiSelector selector,
+ boolean isCounting) {
+ mUiAutomatorBridge.waitForIdle();
+ initializeNewSearch();
+
+ if (DEBUG)
+ Log.d(LOG_TAG, "Searching: " + selector);
+
+ synchronized (mLock) {
+ AccessibilityNodeInfo rootNode = getRootNode();
+ if (rootNode == null) {
+ Log.e(LOG_TAG, "Cannot proceed when root node is null. Aborted search");
+ return null;
+ }
+
+ // Copy so that we don't modify the original's sub selectors
+ UiSelector uiSelector = new UiSelector(selector);
+ return translateCompoundSelector(uiSelector, rootNode, isCounting);
+ }
+ }
+
+ /**
+ * Gets the root node from accessibility and if it fails to get one it will
+ * retry every 250ms for up to 1000ms.
+ * @return null if no root node is obtained
+ */
+ protected AccessibilityNodeInfo getRootNode() {
+ final int maxRetry = 4;
+ final long waitInterval = 250;
+ AccessibilityNodeInfo rootNode = null;
+ for(int x = 0; x < maxRetry; x++) {
+ rootNode = mUiAutomatorBridge.getRootInActiveWindow();
+ if (rootNode != null) {
+ return rootNode;
+ }
+ if(x < maxRetry - 1) {
+ Log.e(LOG_TAG, "Got null root node from accessibility - Retrying...");
+ SystemClock.sleep(waitInterval);
+ }
+ }
+ return rootNode;
+ }
+
+ /**
+ * A compoundSelector encapsulate both Regular and Pattern selectors. The formats follows:
+ * <p/>
+ * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]
+ * <br/>
+ * pattern_selector = ...CONTAINER=By[..] PATTERN=By[instance=x PATTERN=[regular_selector]
+ * <br/>
+ * compound_selector = [regular_selector [pattern_selector]]
+ * <p/>
+ * regular_selectors are the most common form of selectors and the search for them
+ * is straightforward. On the other hand pattern_selectors requires search to be
+ * performed as in regular_selector but where regular_selector search returns immediately
+ * upon a successful match, the search for pattern_selector continues until the
+ * requested matched _instance_ of that pattern is matched.
+ * <p/>
+ * Counting UI objects requires using pattern_selectors. The counting search is the same
+ * as a pattern_search however we're not looking to match an instance of the pattern but
+ * rather continuously walking the accessibility node hierarchy while counting matched
+ * patterns, until the end of the tree.
+ * <p/>
+ * If both present, order of parsing begins with CONTAINER followed by PATTERN then the
+ * top most selector is processed as regular_selector within the context of the previous
+ * CONTAINER and its PATTERN information. If neither is present then the top selector is
+ * directly treated as regular_selector. So the presence of a CONTAINER and PATTERN within
+ * a selector simply dictates that the selector matching will be constraint to the sub tree
+ * node where the CONTAINER and its child PATTERN have identified.
+ * @param selector
+ * @param fromNode
+ * @param isCounting
+ * @return AccessibilityNodeInfo
+ */
+ private AccessibilityNodeInfo translateCompoundSelector(UiSelector selector,
+ AccessibilityNodeInfo fromNode, boolean isCounting) {
+
+ // Start translating compound selectors by translating the regular_selector first
+ // The regular_selector is then used as a container for any optional pattern_selectors
+ // that may or may not be specified.
+ if(selector.hasContainerSelector())
+ // nested pattern selectors
+ if(selector.getContainerSelector().hasContainerSelector()) {
+ fromNode = translateCompoundSelector(
+ selector.getContainerSelector(), fromNode, false);
+ initializeNewSearch();
+ } else
+ fromNode = translateReqularSelector(selector.getContainerSelector(), fromNode);
+ else
+ fromNode = translateReqularSelector(selector, fromNode);
+
+ if(fromNode == null) {
+ if (DEBUG)
+ Log.d(LOG_TAG, "Container selector not found: " + selector.dumpToString(false));
+ return null;
+ }
+
+ if(selector.hasPatternSelector()) {
+ fromNode = translatePatternSelector(selector.getPatternSelector(),
+ fromNode, isCounting);
+
+ if (isCounting) {
+ Log.i(LOG_TAG, String.format(
+ "Counted %d instances of: %s", mPatternCounter, selector));
+ return null;
+ } else {
+ if(fromNode == null) {
+ if (DEBUG)
+ Log.d(LOG_TAG, "Pattern selector not found: " +
+ selector.dumpToString(false));
+ return null;
+ }
+ }
+ }
+
+ // translate any additions to the selector that may have been added by tests
+ // with getChild(By selector) after a container and pattern selectors
+ if(selector.hasContainerSelector() || selector.hasPatternSelector()) {
+ if(selector.hasChildSelector() || selector.hasParentSelector())
+ fromNode = translateReqularSelector(selector, fromNode);
+ }
+
+ if(fromNode == null) {
+ if (DEBUG)
+ Log.d(LOG_TAG, "Object Not Found for selector " + selector);
+ return null;
+ }
+ Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", selector, fromNode));
+ return fromNode;
+ }
+
+ /**
+ * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)}
+ * to translate the regular_selector portion. It has the following format:
+ * <p/>
+ * regular_selector = By[attributes... CHILD=By[attributes... CHILD=By[....]]]<br/>
+ * <p/>
+ * regular_selectors are the most common form of selectors and the search for them
+ * is straightforward. This method will only look for CHILD or PARENT sub selectors.
+ * <p/>
+ * @param selector
+ * @param fromNode
+ * @return AccessibilityNodeInfo if found else null
+ */
+ private AccessibilityNodeInfo translateReqularSelector(UiSelector selector,
+ AccessibilityNodeInfo fromNode) {
+
+ return findNodeRegularRecursive(selector, fromNode, 0);
+ }
+
+ private AccessibilityNodeInfo findNodeRegularRecursive(UiSelector subSelector,
+ AccessibilityNodeInfo fromNode, int index) {
+
+ if (subSelector.isMatchFor(fromNode, index)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, formatLog(String.format("%s",
+ subSelector.dumpToString(false))));
+ }
+ if(subSelector.isLeaf()) {
+ return fromNode;
+ }
+ if(subSelector.hasChildSelector()) {
+ mLogIndent++; // next selector
+ subSelector = subSelector.getChildSelector();
+ if(subSelector == null) {
+ Log.e(LOG_TAG, "Error: A child selector without content");
+ return null; // there is an implementation fault
+ }
+ } else if(subSelector.hasParentSelector()) {
+ mLogIndent++; // next selector
+ subSelector = subSelector.getParentSelector();
+ if(subSelector == null) {
+ Log.e(LOG_TAG, "Error: A parent selector without content");
+ return null; // there is an implementation fault
+ }
+ // the selector requested we start at this level from
+ // the parent node from the one we just matched
+ fromNode = fromNode.getParent();
+ if(fromNode == null)
+ return null;
+ }
+ }
+
+ int childCount = fromNode.getChildCount();
+ boolean hasNullChild = false;
+ for (int i = 0; i < childCount; i++) {
+ AccessibilityNodeInfo childNode = fromNode.getChild(i);
+ if (childNode == null) {
+ Log.w(LOG_TAG, String.format(
+ "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
+ if (!hasNullChild) {
+ Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+ }
+ hasNullChild = true;
+ continue;
+ }
+ if (!childNode.isVisibleToUser()) {
+ if (VERBOSE)
+ Log.v(LOG_TAG,
+ String.format("Skipping invisible child: %s", childNode.toString()));
+ continue;
+ }
+ AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i);
+ if (retNode != null) {
+ return retNode;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Used by the {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)}
+ * to translate the pattern_selector portion. It has the following format:
+ * <p/>
+ * pattern_selector = ... PATTERN=By[instance=x PATTERN=[regular_selector]]<br/>
+ * <p/>
+ * pattern_selectors requires search to be performed as regular_selector but where
+ * regular_selector search returns immediately upon a successful match, the search for
+ * pattern_selector continues until the requested matched instance of that pattern is
+ * encountered.
+ * <p/>
+ * Counting UI objects requires using pattern_selectors. The counting search is the same
+ * as a pattern_search however we're not looking to match an instance of the pattern but
+ * rather continuously walking the accessibility node hierarchy while counting patterns
+ * until the end of the tree.
+ * @param subSelector
+ * @param fromNode
+ * @param isCounting
+ * @return null of node is not found or if counting mode is true.
+ * See {@link #translateCompoundSelector(UiSelector, AccessibilityNodeInfo, boolean)}
+ */
+ private AccessibilityNodeInfo translatePatternSelector(UiSelector subSelector,
+ AccessibilityNodeInfo fromNode, boolean isCounting) {
+
+ if(subSelector.hasPatternSelector()) {
+ // Since pattern_selectors are also the type of selectors used when counting,
+ // we check if this is a counting run or an indexing run
+ if(isCounting)
+ //since we're counting, we reset the indexer so to terminates the search when
+ // the end of tree is reached. The count will be in mPatternCount
+ mPatternIndexer = -1;
+ else
+ // terminates the search once we match the pattern's instance
+ mPatternIndexer = subSelector.getInstance();
+
+ // A pattern is wrapped in a PATTERN[instance=x PATTERN[the_pattern]]
+ subSelector = subSelector.getPatternSelector();
+ if(subSelector == null) {
+ Log.e(LOG_TAG, "Pattern portion of the selector is null or not defined");
+ return null; // there is an implementation fault
+ }
+ // save the current indent level as parent indent before pattern searches
+ // begin under the current tree position.
+ mLogParentIndent = ++mLogIndent;
+ return findNodePatternRecursive(subSelector, fromNode, 0, subSelector);
+ }
+
+ Log.e(LOG_TAG, "Selector must have a pattern selector defined"); // implementation fault?
+ return null;
+ }
+
+ private AccessibilityNodeInfo findNodePatternRecursive(
+ UiSelector subSelector, AccessibilityNodeInfo fromNode, int index,
+ UiSelector originalPattern) {
+
+ if (subSelector.isMatchFor(fromNode, index)) {
+ if(subSelector.isLeaf()) {
+ if(mPatternIndexer == 0) {
+ if (DEBUG)
+ Log.d(LOG_TAG, formatLog(
+ String.format("%s", subSelector.dumpToString(false))));
+ return fromNode;
+ } else {
+ if (DEBUG)
+ Log.d(LOG_TAG, formatLog(
+ String.format("%s", subSelector.dumpToString(false))));
+ mPatternCounter++; //count the pattern matched
+ mPatternIndexer--; //decrement until zero for the instance requested
+
+ // At a leaf selector within a group and still not instance matched
+ // then reset the selector to continue search from current position
+ // in the accessibility tree for the next pattern match up until the
+ // pattern index hits 0.
+ subSelector = originalPattern;
+ // starting over with next pattern search so reset to parent level
+ mLogIndent = mLogParentIndent;
+ }
+ } else {
+ if (DEBUG)
+ Log.d(LOG_TAG, formatLog(
+ String.format("%s", subSelector.dumpToString(false))));
+
+ if(subSelector.hasChildSelector()) {
+ mLogIndent++; // next selector
+ subSelector = subSelector.getChildSelector();
+ if(subSelector == null) {
+ Log.e(LOG_TAG, "Error: A child selector without content");
+ return null;
+ }
+ } else if(subSelector.hasParentSelector()) {
+ mLogIndent++; // next selector
+ subSelector = subSelector.getParentSelector();
+ if(subSelector == null) {
+ Log.e(LOG_TAG, "Error: A parent selector without content");
+ return null;
+ }
+ fromNode = fromNode.getParent();
+ if(fromNode == null)
+ return null;
+ }
+ }
+ }
+
+ int childCount = fromNode.getChildCount();
+ boolean hasNullChild = false;
+ for (int i = 0; i < childCount; i++) {
+ AccessibilityNodeInfo childNode = fromNode.getChild(i);
+ if (childNode == null) {
+ Log.w(LOG_TAG, String.format(
+ "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
+ if (!hasNullChild) {
+ Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+ }
+ hasNullChild = true;
+ continue;
+ }
+ if (!childNode.isVisibleToUser()) {
+ if (DEBUG)
+ Log.d(LOG_TAG,
+ String.format("Skipping invisible child: %s", childNode.toString()));
+ continue;
+ }
+ AccessibilityNodeInfo retNode = findNodePatternRecursive(
+ subSelector, childNode, i, originalPattern);
+ if (retNode != null) {
+ return retNode;
+ }
+ }
+ return null;
+ }
+
+ public AccessibilityNodeInfo getAccessibilityRootNode() {
+ return mUiAutomatorBridge.getRootInActiveWindow();
+ }
+
+ /**
+ * Last activity to report accessibility events.
+ * @deprecated The results returned should be considered unreliable
+ * @return String name of activity
+ */
+ @Deprecated
+ public String getCurrentActivityName() {
+ mUiAutomatorBridge.waitForIdle();
+ synchronized (mLock) {
+ return mLastActivityName;
+ }
+ }
+
+ /**
+ * Last package to report accessibility events
+ * @return String name of package
+ */
+ public String getCurrentPackageName() {
+ mUiAutomatorBridge.waitForIdle();
+ AccessibilityNodeInfo rootNode = getRootNode();
+ if (rootNode == null)
+ return null;
+ return rootNode.getPackageName() != null ? rootNode.getPackageName().toString() : null;
+ }
+
+ private String formatLog(String str) {
+ StringBuilder l = new StringBuilder();
+ for(int space = 0; space < mLogIndent; space++)
+ l.append(". . ");
+ if(mLogIndent > 0)
+ l.append(String.format(". . [%d]: %s", mPatternCounter, str));
+ else
+ l.append(String.format(". . [%d]: %s", mPatternCounter, str));
+ return l.toString();
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java
new file mode 100644
index 0000000..d574fc0
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java
@@ -0,0 +1,285 @@
+/*
+ * 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.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Class that creates traces of the calls to the UiAutomator API and outputs the
+ * traces either to logcat or a logfile. Each public method in the UiAutomator
+ * that needs to be traced should include a call to Tracer.trace in the
+ * beginning. Tracing is turned off by defualt and needs to be enabled
+ * explicitly.
+ * @hide
+ */
+public class Tracer {
+ private static final String UNKNOWN_METHOD_STRING = "(unknown method)";
+ private static final String UIAUTOMATOR_PACKAGE = "com.android.uiautomator.core";
+ private static final int CALLER_LOCATION = 6;
+ private static final int METHOD_TO_TRACE_LOCATION = 5;
+ private static final int MIN_STACK_TRACE_LENGTH = 7;
+
+ /**
+ * Enum that determines where the trace output goes. It can go to either
+ * logcat, log file or both.
+ */
+ public enum Mode {
+ NONE,
+ FILE,
+ LOGCAT,
+ ALL
+ }
+
+ private interface TracerSink {
+ public void log(String message);
+
+ public void close();
+ }
+
+ private class FileSink implements TracerSink {
+ private PrintWriter mOut;
+ private SimpleDateFormat mDateFormat;
+
+ public FileSink(File file) throws FileNotFoundException {
+ mOut = new PrintWriter(file);
+ mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
+ }
+
+ public void log(String message) {
+ mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
+ }
+
+ public void close() {
+ mOut.close();
+ }
+ }
+
+ private class LogcatSink implements TracerSink {
+
+ private static final String LOGCAT_TAG = "UiAutomatorTrace";
+
+ public void log(String message) {
+ Log.i(LOGCAT_TAG, message);
+ }
+
+ public void close() {
+ // nothing is needed
+ }
+ }
+
+ private Mode mCurrentMode = Mode.NONE;
+ private List<TracerSink> mSinks = new ArrayList<TracerSink>();
+ private File mOutputFile;
+
+ private static Tracer mInstance = null;
+
+ /**
+ * Returns a reference to an instance of the tracer. Useful to set the
+ * parameters before the trace is collected.
+ *
+ * @return
+ */
+ public static Tracer getInstance() {
+ if (mInstance == null) {
+ mInstance = new Tracer();
+ }
+ return mInstance;
+ }
+
+ /**
+ * Sets where the trace output will go. Can be either be logcat or a file or
+ * both. Setting this to NONE will turn off tracing.
+ *
+ * @param mode
+ */
+ public void setOutputMode(Mode mode) {
+ closeSinks();
+ mCurrentMode = mode;
+ try {
+ switch (mode) {
+ case FILE:
+ if (mOutputFile == null) {
+ throw new IllegalArgumentException("Please provide a filename before " +
+ "attempting write trace to a file");
+ }
+ mSinks.add(new FileSink(mOutputFile));
+ break;
+ case LOGCAT:
+ mSinks.add(new LogcatSink());
+ break;
+ case ALL:
+ mSinks.add(new LogcatSink());
+ if (mOutputFile == null) {
+ throw new IllegalArgumentException("Please provide a filename before " +
+ "attempting write trace to a file");
+ }
+ mSinks.add(new FileSink(mOutputFile));
+ break;
+ default:
+ break;
+ }
+ } catch (FileNotFoundException e) {
+ Log.w("Tracer", "Could not open log file: " + e.getMessage());
+ }
+ }
+
+ private void closeSinks() {
+ for (TracerSink sink : mSinks) {
+ sink.close();
+ }
+ mSinks.clear();
+ }
+
+ /**
+ * Sets the name of the log file where tracing output will be written if the
+ * tracer is set to write to a file.
+ *
+ * @param filename name of the log file.
+ */
+ public void setOutputFilename(String filename) {
+ mOutputFile = new File(filename);
+ }
+
+ private void doTrace(Object[] arguments) {
+ if (mCurrentMode == Mode.NONE) {
+ return;
+ }
+
+ String caller = getCaller();
+ if (caller == null) {
+ return;
+ }
+
+ log(String.format("%s (%s)", caller, join(", ", arguments)));
+ }
+
+ private void log(String message) {
+ for (TracerSink sink : mSinks) {
+ sink.log(message);
+ }
+ }
+
+ /**
+ * Queries whether the tracing is enabled.
+ * @return true if tracing is enabled, false otherwise.
+ */
+ public boolean isTracingEnabled() {
+ return mCurrentMode != Mode.NONE;
+ }
+
+ /**
+ * Public methods in the UiAutomator should call this function to generate a
+ * trace. The trace will include the method thats is being called, it's
+ * arguments and where in the user's code the method is called from. If a
+ * public method is called internally from UIAutomator then this will not
+ * output a trace entry. Only calls from outise the UiAutomator package will
+ * produce output.
+ *
+ * Special note about array arguments. You can safely pass arrays of reference types
+ * to this function. Like String[] or Integer[]. The trace function will print their
+ * contents by calling toString() on each of the elements. This will not work for
+ * array of primitive types like int[] or float[]. Before passing them to this function
+ * convert them to arrays of reference types manually. Example: convert int[] to Integer[].
+ *
+ * @param arguments arguments of the method being traced.
+ */
+ public static void trace(Object... arguments) {
+ Tracer.getInstance().doTrace(arguments);
+ }
+
+ private static String join(String separator, Object[] strings) {
+ if (strings.length == 0)
+ return "";
+
+ StringBuilder builder = new StringBuilder(objectToString(strings[0]));
+ for (int i = 1; i < strings.length; i++) {
+ builder.append(separator);
+ builder.append(objectToString(strings[i]));
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Special toString method to handle arrays. If the argument is a normal object then this will
+ * return normal output of obj.toString(). If the argument is an array this will return a
+ * string representation of the elements of the array.
+ *
+ * This method will not work for arrays of primitive types. Arrays of primitive types are
+ * expected to be converted manually by the caller. If the array is not converter then
+ * this function will only output "[...]" instead of the contents of the array.
+ *
+ * @param obj object to convert to a string
+ * @return String representation of the object.
+ */
+ private static String objectToString(Object obj) {
+ if (obj.getClass().isArray()) {
+ if (obj instanceof Object[]) {
+ return Arrays.deepToString((Object[])obj);
+ } else {
+ return "[...]";
+ }
+ } else {
+ return obj.toString();
+ }
+ }
+
+ /**
+ * This method outputs which UiAutomator method was called and where in the
+ * user code it was called from. If it can't deside which method is called
+ * it will output "(unknown method)". If the method was called from inside
+ * the UiAutomator then it returns null.
+ *
+ * @return name of the method called and where it was called from. Null if
+ * method was called from inside UiAutomator.
+ */
+ private static String getCaller() {
+ StackTraceElement stackTrace[] = Thread.currentThread().getStackTrace();
+ if (stackTrace.length < MIN_STACK_TRACE_LENGTH) {
+ return UNKNOWN_METHOD_STRING;
+ }
+
+ StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION];
+ StackTraceElement previousCaller = stackTrace[CALLER_LOCATION];
+
+ if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) {
+ return null;
+ }
+
+ int indexOfDot = caller.getClassName().lastIndexOf('.');
+ if (indexOfDot < 0) {
+ indexOfDot = 0;
+ }
+
+ if (indexOfDot + 1 >= caller.getClassName().length()) {
+ return UNKNOWN_METHOD_STRING;
+ }
+
+ String shortClassName = caller.getClassName().substring(indexOfDot + 1);
+ return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(),
+ previousCaller.getMethodName(), previousCaller.getFileName(),
+ previousCaller.getLineNumber());
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java
new file mode 100644
index 0000000..bc5bc8e
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java
@@ -0,0 +1,143 @@
+package com.android.uiautomator.core;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.app.UiAutomation.OnAccessibilityEventListener;
+import android.graphics.Bitmap;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @hide
+ */
+public abstract class UiAutomatorBridge {
+
+ private static final String LOG_TAG = UiAutomatorBridge.class.getSimpleName();
+
+ /**
+ * This value has the greatest bearing on the appearance of test execution speeds.
+ * This value is used as the minimum time to wait before considering the UI idle after
+ * each action.
+ */
+ private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;//ms
+
+ /**
+ * This is the maximum time the automation will wait for the UI to go idle. Execution
+ * will resume normally anyway. This is to prevent waiting forever on display updates
+ * that may be related to spinning wheels or progress updates of sorts etc...
+ */
+ private static final long TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE = 1000 * 10;//ms
+
+ private final UiAutomation mUiAutomation;
+
+ private final InteractionController mInteractionController;
+
+ private final QueryController mQueryController;
+
+ UiAutomatorBridge(UiAutomation uiAutomation) {
+ mUiAutomation = uiAutomation;
+ mInteractionController = new InteractionController(this);
+ mQueryController = new QueryController(this);
+ }
+
+ InteractionController getInteractionController() {
+ return mInteractionController;
+ }
+
+ QueryController getQueryController() {
+ return mQueryController;
+ }
+
+ public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
+ mUiAutomation.setOnAccessibilityEventListener(listener);
+ }
+
+ public AccessibilityNodeInfo getRootInActiveWindow() {
+ return mUiAutomation.getRootInActiveWindow();
+ }
+
+ public boolean injectInputEvent(InputEvent event, boolean sync) {
+ return mUiAutomation.injectInputEvent(event, sync);
+ }
+
+ public boolean setRotation(int rotation) {
+ return mUiAutomation.setRotation(rotation);
+ }
+
+ public void setCompressedLayoutHierarchy(boolean compressed) {
+ AccessibilityServiceInfo info = mUiAutomation.getServiceInfo();
+ if (compressed)
+ info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ else
+ info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+ mUiAutomation.setServiceInfo(info);
+ }
+
+ public abstract int getRotation();
+
+ public abstract boolean isScreenOn();
+
+ public void waitForIdle() {
+ waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE);
+ }
+
+ public void waitForIdle(long timeout) {
+ try {
+ mUiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout);
+ } catch (TimeoutException te) {
+ Log.w(LOG_TAG, "Could not detect idle state.", te);
+ }
+ }
+
+ public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command,
+ AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
+ return mUiAutomation.executeAndWaitForEvent(command,
+ filter, timeoutMillis);
+ }
+
+ public boolean takeScreenshot(File storePath, int quality) {
+ Bitmap screenshot = mUiAutomation.takeScreenshot();
+ if (screenshot == null) {
+ return false;
+ }
+ BufferedOutputStream bos = null;
+ try {
+ bos = new BufferedOutputStream(new FileOutputStream(storePath));
+ if (bos != null) {
+ screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos);
+ bos.flush();
+ }
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "failed to save screen shot to file", ioe);
+ return false;
+ } finally {
+ if (bos != null) {
+ try {
+ bos.close();
+ } catch (IOException ioe) {
+ /* ignore */
+ }
+ }
+ screenshot.recycle();
+ }
+ return true;
+ }
+
+ public boolean performGlobalAction(int action) {
+ return mUiAutomation.performGlobalAction(action);
+ }
+
+ public abstract Display getDefaultDisplay();
+
+ public abstract long getSystemLongPressTime();
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java
new file mode 100644
index 0000000..e15beb2
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java
@@ -0,0 +1,145 @@
+/*
+ * 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;
+
+/**
+ * Used to enumerate a container's UI elements for the purpose of counting,
+ * or targeting a sub elements by a child's text or description.
+ * @since API Level 16
+ */
+public class UiCollection extends UiObject {
+
+ /**
+ * Constructs an instance as described by the selector
+ *
+ * @param selector
+ * @since API Level 16
+ */
+ public UiCollection(UiSelector selector) {
+ super(selector);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiCollection {@link UiSelector}
+ * selector.
+ *
+ * It looks for any child matching the <code>childPattern</code> argument that has
+ * a child UI element anywhere within its sub hierarchy that has content-description text.
+ * The returned UiObject will point at the <code>childPattern</code> instance that matched the
+ * search and not at the identifying child element that matched the content description.</p>
+ *
+ * @param childPattern {@link UiSelector} selector of the child pattern to match and return
+ * @param text String of the identifying child contents of of the <code>childPattern</code>
+ * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public UiObject getChildByDescription(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ if (text != null) {
+ int count = getChildCount(childPattern);
+ for (int x = 0; x < count; x++) {
+ UiObject row = getChildByInstance(childPattern, x);
+ String nodeDesc = row.getContentDescription();
+ if(nodeDesc != null && nodeDesc.contains(text)) {
+ return row;
+ }
+ UiObject item = row.getChild(new UiSelector().descriptionContains(text));
+ if (item.exists()) {
+ return row;
+ }
+ }
+ }
+ throw new UiObjectNotFoundException("for description= \"" + text + "\"");
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiCollection {@link UiSelector}
+ * selector.
+ *
+ * It looks for any child matching the <code>childPattern</code> argument that has
+ * a child UI element anywhere within its sub hierarchy that is at the <code>instance</code>
+ * specified. The operation is performed only on the visible items and no scrolling is performed
+ * in this case.
+ *
+ * @param childPattern {@link UiSelector} selector of the child pattern to match and return
+ * @param instance int the desired matched instance of this <code>childPattern</code>
+ * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
+ * @since API Level 16
+ */
+ public UiObject getChildByInstance(UiSelector childPattern, int instance)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, instance);
+ UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
+ UiSelector.patternBuilder(childPattern).instance(instance));
+ return new UiObject(patternSelector);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiCollection {@link UiSelector}
+ * selector.
+ *
+ * It looks for any child matching the <code>childPattern</code> argument that has
+ * a child UI element anywhere within its sub hierarchy that has text attribute =
+ * <code>text</code>. The returned UiObject will point at the <code>childPattern</code>
+ * instance that matched the search and not at the identifying child element that matched the
+ * text attribute.</p>
+ *
+ * @param childPattern {@link UiSelector} selector of the child pattern to match and return
+ * @param text String of the identifying child contents of of the <code>childPattern</code>
+ * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public UiObject getChildByText(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ if (text != null) {
+ int count = getChildCount(childPattern);
+ for (int x = 0; x < count; x++) {
+ UiObject row = getChildByInstance(childPattern, x);
+ String nodeText = row.getText();
+ if(text.equals(nodeText)) {
+ return row;
+ }
+ UiObject item = row.getChild(new UiSelector().text(text));
+ if (item.exists()) {
+ return row;
+ }
+ }
+ }
+ throw new UiObjectNotFoundException("for text= \"" + text + "\"");
+ }
+
+ /**
+ * Counts child UI element instances matching the <code>childPattern</code>
+ * argument. The method returns the number of matching UI elements that are
+ * currently visible. The count does not include items of a scrollable list
+ * that are off-screen.
+ *
+ * @param childPattern a {@link UiSelector} that represents the matching child UI
+ * elements to count
+ * @return the number of matched childPattern under the current {@link UiCollection}
+ * @since API Level 16
+ */
+ public int getChildCount(UiSelector childPattern) {
+ Tracer.trace(childPattern);
+ UiSelector patternSelector =
+ UiSelector.patternBuilder(getSelector(), UiSelector.patternBuilder(childPattern));
+ return getQueryController().getPatternCount(patternSelector);
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
new file mode 100644
index 0000000..a930eb4
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
@@ -0,0 +1,851 @@
+/*
+ * 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.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.graphics.Point;
+import android.os.Build;
+import android.os.Environment;
+import android.os.RemoteException;
+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 java.io.File;
+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();
+
+ // 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 UiAutomatorBridge mUiAutomationBridge;
+
+ // reference to self
+ private static UiDevice sDevice;
+
+ private UiDevice() {
+ /* hide constructor */
+ }
+
+ /**
+ * @hide
+ */
+ public void initialize(UiAutomatorBridge uiAutomatorBridge) {
+ mUiAutomationBridge = uiAutomatorBridge;
+ }
+
+ boolean isInWatcherContext() {
+ return mInWatcherContext;
+ }
+
+ /**
+ * Provides access the {@link QueryController} and {@link InteractionController}
+ * @return {@link ShellUiAutomatorBridge}
+ */
+ UiAutomatorBridge getAutomatorBridge() {
+ if (mUiAutomationBridge == null) {
+ throw new RuntimeException("UiDevice not initialized");
+ }
+ return mUiAutomationBridge;
+ }
+
+ /**
+ * Enables or disables layout hierarchy compression.
+ *
+ * If compression is enabled, the layout hierarchy derived from the Acessibility
+ * framework will only contain nodes that are important for uiautomator
+ * testing. Any unnecessary surrounding layout nodes that make viewing
+ * and searching the hierarchy inefficient are removed.
+ *
+ * @param compressed true to enable compression; else, false to disable
+ * @since API Level 18
+ */
+ public void setCompressedLayoutHeirarchy(boolean compressed) {
+ getAutomatorBridge().setCompressedLayoutHierarchy(compressed);
+ }
+
+ /**
+ * Retrieves a singleton instance of UiDevice
+ *
+ * @return UiDevice instance
+ * @since API Level 16
+ */
+ public static UiDevice getInstance() {
+ if (sDevice == null) {
+ sDevice = new UiDevice();
+ }
+ return sDevice;
+ }
+
+ /**
+ * Returns the display size in dp (device-independent pixel)
+ *
+ * The returned display size is adjusted per screen rotation. Also this will return the actual
+ * size of the screen, rather than adjusted per system decorations (like status bar).
+ *
+ * @return a Point containing the display size in dp
+ */
+ public Point getDisplaySizeDp() {
+ Tracer.trace();
+ Display display = getAutomatorBridge().getDefaultDisplay();
+ Point p = new Point();
+ display.getRealSize(p);
+ DisplayMetrics metrics = new DisplayMetrics();
+ display.getRealMetrics(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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ return getAutomatorBridge().getQueryController().getLastTraversedText();
+ }
+
+ /**
+ * Clears the text from the last UI traversal event.
+ * See {@link #getLastTraversedText()}.
+ * @since API Level 16
+ */
+ public void clearLastTraversedText() {
+ Tracer.trace();
+ getAutomatorBridge().getQueryController().clearLastTraversedText();
+ }
+
+ /**
+ * Simulates a short press on the MENU button.
+ * @return true if successful, else return false
+ * @since API Level 16
+ */
+ public boolean pressMenu() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().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() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().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() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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) {
+ Tracer.trace(keyCode);
+ waitForIdle();
+ return getAutomatorBridge().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) {
+ Tracer.trace(keyCode, metaState);
+ waitForIdle();
+ return getAutomatorBridge().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 {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().getInteractionController().toggleRecentApps();
+ }
+
+ /**
+ * Opens the notification shade.
+ *
+ * @return true if successful, else return false
+ * @since API Level 18
+ */
+ public boolean openNotification() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().getInteractionController().openNotification();
+ }
+
+ /**
+ * Opens the Quick Settings shade.
+ *
+ * @return true if successful, else return false
+ * @since API Level 18
+ */
+ public boolean openQuickSettings() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().getInteractionController().openQuickSettings();
+ }
+
+ /**
+ * 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() {
+ Tracer.trace();
+ Display display = getAutomatorBridge().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() {
+ Tracer.trace();
+ Display display = getAutomatorBridge().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) {
+ Tracer.trace(x, y);
+ if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
+ return (false);
+ }
+ return getAutomatorBridge().getInteractionController().clickNoSync(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) {
+ Tracer.trace(startX, startY, endX, endY, steps);
+ return getAutomatorBridge().getInteractionController()
+ .swipe(startX, startY, endX, endY, steps);
+ }
+
+ /**
+ * Performs a swipe from one coordinate to another coordinate. You can control
+ * the smoothness and speed of the swipe by specifying the number of steps.
+ * Each step execution is throttled to 5 milliseconds per step, so for a 100
+ * steps, the swipe will take around 0.5 seconds to complete.
+ *
+ * @param startX X-axis value for the starting coordinate
+ * @param startY Y-axis value for the starting coordinate
+ * @param endX X-axis value for the ending coordinate
+ * @param endY Y-axis value for the ending coordinate
+ * @param steps is the number of steps for the swipe action
+ * @return true if swipe is performed, false if the operation fails
+ * or the coordinates are invalid
+ * @since API Level 18
+ */
+ public boolean drag(int startX, int startY, int endX, int endY, int steps) {
+ Tracer.trace(startX, startY, endX, endY, steps);
+ return getAutomatorBridge().getInteractionController()
+ .swipe(startX, startY, endX, endY, steps, true);
+ }
+
+ /**
+ * 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) {
+ Tracer.trace(segments, segmentSteps);
+ return getAutomatorBridge().getInteractionController().swipe(segments, segmentSteps);
+ }
+
+ /**
+ * Waits for the current application to idle.
+ * Default wait timeout is 10 seconds
+ * @since API Level 16
+ */
+ public void waitForIdle() {
+ Tracer.trace();
+ waitForIdle(Configurator.getInstance().getWaitForIdleTimeout());
+ }
+
+ /**
+ * Waits for the current application to idle.
+ * @param timeout in milliseconds
+ * @since API Level 16
+ */
+ public void waitForIdle(long timeout) {
+ Tracer.trace(timeout);
+ getAutomatorBridge().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() {
+ Tracer.trace();
+ return getAutomatorBridge().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() {
+ Tracer.trace();
+ return getAutomatorBridge().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) {
+ Tracer.trace(name, 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) {
+ Tracer.trace(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() {
+ Tracer.trace();
+ 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() {
+ Tracer.trace();
+ 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) {
+ Tracer.trace(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() {
+ Tracer.trace();
+ return mWatchersTriggers.size() > 0;
+ }
+
+ /**
+ * Used internally by this class to set a {@link UiWatcher} state as triggered.
+ * @param watcherName
+ */
+ private void setWatcherTriggered(String watcherName) {
+ Tracer.trace(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() {
+ Tracer.trace();
+ waitForIdle();
+ int ret = getAutomatorBridge().getRotation();
+ return ret == UiAutomation.ROTATION_FREEZE_0 ||
+ ret == UiAutomation.ROTATION_FREEZE_180;
+ }
+
+ /**
+ * Returns the current rotation of the display, as defined in {@link Surface}
+ * @since API Level 17
+ */
+ public int getDisplayRotation() {
+ Tracer.trace();
+ waitForIdle();
+ return getAutomatorBridge().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 {
+ Tracer.trace();
+ 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 {
+ Tracer.trace();
+ 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 {
+ Tracer.trace();
+ getAutomatorBridge().getInteractionController().setRotationLeft();
+ waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ }
+
+ /**
+ * 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 {
+ Tracer.trace();
+ getAutomatorBridge().getInteractionController().setRotationRight();
+ waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ }
+
+ /**
+ * 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 {
+ Tracer.trace();
+ getAutomatorBridge().getInteractionController().setRotationNatural();
+ waitForIdle(); // we don't need to check for idle on entry for this. We'll sync on exit
+ }
+
+ /**
+ * 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 {
+ Tracer.trace();
+ 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 {
+ Tracer.trace();
+ 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 {
+ Tracer.trace();
+ 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) {
+ Tracer.trace(fileName);
+ AccessibilityNodeInfo root =
+ getAutomatorBridge().getQueryController().getAccessibilityRootNode();
+ if(root != null) {
+ Display display = getAutomatorBridge().getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ AccessibilityNodeInfoDumper.dumpWindowToFile(root,
+ new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
+ display.getRotation(), size.x, size.y);
+ }
+ }
+
+ /**
+ * 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) {
+ Tracer.trace(packageName, timeout);
+ if (packageName != null) {
+ if (!packageName.equals(getCurrentPackageName())) {
+ return false;
+ }
+ }
+ Runnable emptyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+ AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() {
+ @Override
+ public boolean accept(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;
+ }
+
+ /**
+ * 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) {
+ Tracer.trace(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) {
+ Tracer.trace(storePath, scale, quality);
+ return getAutomatorBridge().takeScreenshot(storePath, quality);
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
new file mode 100644
index 0000000..4bb99cd
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
@@ -0,0 +1,1083 @@
+/*
+ * 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.graphics.Point;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * A UiObject is a representation of a view. It is not in any way directly bound to a
+ * view as an object reference. A UiObject contains information to help it
+ * locate a matching view at runtime based on the {@link UiSelector} properties specified in
+ * its constructor. Once you create an instance of a UiObject, it can
+ * be reused for different views that match the selector criteria.
+ * @since API Level 16
+ */
+public class UiObject {
+ private static final String LOG_TAG = UiObject.class.getSimpleName();
+ /**
+ * @since API Level 16
+ * @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)}
+ **/
+ @Deprecated
+ protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000;
+ /**
+ * @since API Level 16
+ **/
+ protected static final long WAIT_FOR_SELECTOR_POLL = 1000;
+ // set a default timeout to 5.5s, since ANR threshold is 5s
+ /**
+ * @since API Level 16
+ **/
+ protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500;
+ /**
+ * @since API Level 16
+ **/
+ protected static final int SWIPE_MARGIN_LIMIT = 5;
+ /**
+ * @since API Level 17
+ * @deprecated use {@link Configurator#setScrollAcknowledgmentTimeout(long)}
+ **/
+ @Deprecated
+ protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000;
+ /**
+ * @since API Level 18
+ **/
+ protected static final int FINGER_TOUCH_HALF_WIDTH = 20;
+
+ private final UiSelector mSelector;
+
+ private final Configurator mConfig = Configurator.getInstance();
+
+ /**
+ * Constructs a UiObject to represent a view that matches the specified
+ * selector criteria.
+ * @param selector
+ * @since API Level 16
+ */
+ public UiObject(UiSelector selector) {
+ mSelector = selector;
+ }
+
+ /**
+ * Debugging helper. A test can dump the properties of a selector as a string
+ * to its logs if needed. <code>getSelector().toString();</code>
+ *
+ * @return {@link UiSelector}
+ * @since API Level 16
+ */
+ public final UiSelector getSelector() {
+ Tracer.trace();
+ return new UiSelector(mSelector);
+ }
+
+ /**
+ * Retrieves the {@link QueryController} to translate a {@link UiSelector} selector
+ * into an {@link AccessibilityNodeInfo}.
+ *
+ * @return {@link QueryController}
+ */
+ QueryController getQueryController() {
+ return UiDevice.getInstance().getAutomatorBridge().getQueryController();
+ }
+
+ /**
+ * Retrieves the {@link InteractionController} to perform finger actions such as tapping,
+ * swiping, or entering text.
+ *
+ * @return {@link InteractionController}
+ */
+ InteractionController getInteractionController() {
+ return UiDevice.getInstance().getAutomatorBridge().getInteractionController();
+ }
+
+ /**
+ * Creates a new UiObject for a child view that is under the present UiObject.
+ *
+ * @param selector for child view to match
+ * @return a new UiObject representing the child view
+ * @since API Level 16
+ */
+ public UiObject getChild(UiSelector selector) throws UiObjectNotFoundException {
+ Tracer.trace(selector);
+ return new UiObject(getSelector().childSelector(selector));
+ }
+
+ /**
+ * Creates a new UiObject for a sibling view or a child of the sibling view,
+ * relative to the present UiObject.
+ *
+ * @param selector for a sibling view or children of the sibling view
+ * @return a new UiObject representing the matched view
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public UiObject getFromParent(UiSelector selector) throws UiObjectNotFoundException {
+ Tracer.trace(selector);
+ return new UiObject(getSelector().fromParent(selector));
+ }
+
+ /**
+ * Counts the child views immediately under the present UiObject.
+ *
+ * @return the count of child views.
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public int getChildCount() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.getChildCount();
+ }
+
+ /**
+ * Finds a matching UI element in the accessibility hierarchy, by
+ * using the selector for this UiObject.
+ *
+ * @param timeout in milliseconds
+ * @return AccessibilityNodeInfo if found else null
+ * @since API Level 16
+ */
+ protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) {
+ AccessibilityNodeInfo node = null;
+ long startMills = SystemClock.uptimeMillis();
+ long currentMills = 0;
+ while (currentMills <= timeout) {
+ node = getQueryController().findAccessibilityNodeInfo(getSelector());
+ if (node != null) {
+ break;
+ } else {
+ // does nothing if we're reentering another runWatchers()
+ UiDevice.getInstance().runWatchers();
+ }
+ currentMills = SystemClock.uptimeMillis() - startMills;
+ if(timeout > 0) {
+ SystemClock.sleep(WAIT_FOR_SELECTOR_POLL);
+ }
+ }
+ return node;
+ }
+
+ /**
+ * Drags this object to a destination UiObject.
+ * The number of steps specified in your input parameter can influence the
+ * drag speed, and varying speeds may impact the results. Consider
+ * evaluating different speeds when using this method in your tests.
+ *
+ * @param destObj the destination UiObject.
+ * @param steps usually 40 steps. You can increase or decrease the steps to change the speed.
+ * @return true if successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(UiObject destObj, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ Rect dstRect = destObj.getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(),
+ dstRect.centerX(), dstRect.centerY(), steps, true);
+ }
+
+ /**
+ * Drags this object to arbitrary coordinates.
+ * The number of steps specified in your input parameter can influence the
+ * drag speed, and varying speeds may impact the results. Consider
+ * evaluating different speeds when using this method in your tests.
+ *
+ * @param destX the X-axis coordinate.
+ * @param destY the Y-axis coordinate.
+ * @param steps usually 40 steps. You can increase or decrease the steps to change the speed.
+ * @return true if successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY,
+ steps, true);
+ }
+
+ /**
+ * Performs the swipe up action on the UiObject.
+ * See also:
+ * <ul>
+ * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
+ * <li>{@link UiScrollable#scrollToEnd(int)}</li>
+ * <li>{@link UiScrollable#scrollBackward()}</li>
+ * <li>{@link UiScrollable#scrollForward()}</li>
+ * </ul>
+ *
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @return true of successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean swipeUp(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Rect rect = getVisibleBounds();
+ if(rect.height() <= SWIPE_MARGIN_LIMIT * 2)
+ return false; // too small to swipe
+ return getInteractionController().swipe(rect.centerX(),
+ rect.bottom - SWIPE_MARGIN_LIMIT, rect.centerX(), rect.top + SWIPE_MARGIN_LIMIT,
+ steps);
+ }
+
+ /**
+ * Performs the swipe down action on the UiObject.
+ * The swipe gesture can be performed over any surface. The targeted
+ * UI element does not need to be scrollable.
+ * See also:
+ * <ul>
+ * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
+ * <li>{@link UiScrollable#scrollToEnd(int)}</li>
+ * <li>{@link UiScrollable#scrollBackward()}</li>
+ * <li>{@link UiScrollable#scrollForward()}</li>
+ * </ul>
+ *
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @return true if successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean swipeDown(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Rect rect = getVisibleBounds();
+ if(rect.height() <= SWIPE_MARGIN_LIMIT * 2)
+ return false; // too small to swipe
+ return getInteractionController().swipe(rect.centerX(),
+ rect.top + SWIPE_MARGIN_LIMIT, rect.centerX(),
+ rect.bottom - SWIPE_MARGIN_LIMIT, steps);
+ }
+
+ /**
+ * Performs the swipe left action on the UiObject.
+ * The swipe gesture can be performed over any surface. The targeted
+ * UI element does not need to be scrollable.
+ * See also:
+ * <ul>
+ * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
+ * <li>{@link UiScrollable#scrollToEnd(int)}</li>
+ * <li>{@link UiScrollable#scrollBackward()}</li>
+ * <li>{@link UiScrollable#scrollForward()}</li>
+ * </ul>
+ *
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @return true if successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean swipeLeft(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Rect rect = getVisibleBounds();
+ if(rect.width() <= SWIPE_MARGIN_LIMIT * 2)
+ return false; // too small to swipe
+ return getInteractionController().swipe(rect.right - SWIPE_MARGIN_LIMIT,
+ rect.centerY(), rect.left + SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
+ }
+
+ /**
+ * Performs the swipe right action on the UiObject.
+ * The swipe gesture can be performed over any surface. The targeted
+ * UI element does not need to be scrollable.
+ * See also:
+ * <ul>
+ * <li>{@link UiScrollable#scrollToBeginning(int)}</li>
+ * <li>{@link UiScrollable#scrollToEnd(int)}</li>
+ * <li>{@link UiScrollable#scrollBackward()}</li>
+ * <li>{@link UiScrollable#scrollForward()}</li>
+ * </ul>
+ *
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @return true if successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean swipeRight(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Rect rect = getVisibleBounds();
+ if(rect.width() <= SWIPE_MARGIN_LIMIT * 2)
+ return false; // too small to swipe
+ return getInteractionController().swipe(rect.left + SWIPE_MARGIN_LIMIT,
+ rect.centerY(), rect.right - SWIPE_MARGIN_LIMIT, rect.centerY(), steps);
+ }
+
+ /**
+ * Finds the visible bounds of a partially visible UI element
+ *
+ * @param node
+ * @return null if node is null, else a Rect containing visible bounds
+ */
+ private Rect getVisibleBounds(AccessibilityNodeInfo node) {
+ if (node == null) {
+ return null;
+ }
+
+ // targeted node's bounds
+ int w = UiDevice.getInstance().getDisplayWidth();
+ int h = UiDevice.getInstance().getDisplayHeight();
+ Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
+
+ // is the targeted node within a scrollable container?
+ AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node);
+ if(scrollableParentNode == null) {
+ // nothing to adjust for so return the node's Rect as is
+ return nodeRect;
+ }
+
+ // Scrollable parent's visible bounds
+ Rect parentRect = AccessibilityNodeInfoHelper
+ .getVisibleBoundsInScreen(scrollableParentNode, w, h);
+ // adjust for partial clipping of targeted by parent node if required
+ nodeRect.intersect(parentRect);
+ return nodeRect;
+ }
+
+ /**
+ * Walks up the layout hierarchy to find a scrollable parent. A scrollable parent
+ * indicates that this node might be in a container where it is partially
+ * visible due to scrolling. In this case, its clickable center might not be visible and
+ * the click coordinates should be adjusted.
+ *
+ * @param node
+ * @return The accessibility node info.
+ */
+ private AccessibilityNodeInfo getScrollableParent(AccessibilityNodeInfo node) {
+ AccessibilityNodeInfo parent = node;
+ while(parent != null) {
+ parent = parent.getParent();
+ if (parent != null && parent.isScrollable()) {
+ return parent;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Performs a click at the center of the visible bounds of the UI element represented
+ * by this UiObject.
+ *
+ * @return true id successful else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean click() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(),
+ mConfig.getActionAcknowledgmentTimeout());
+ }
+
+ /**
+ * Waits for window transitions that would typically take longer than the
+ * usual default timeouts.
+ * See {@link #clickAndWaitForNewWindow(long)}
+ *
+ * @return true if the event was triggered, else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException {
+ Tracer.trace();
+ return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT);
+ }
+
+ /**
+ * Performs a click at the center of the visible bounds of the UI element represented
+ * by this UiObject and waits for window transitions.
+ *
+ * This method differ from {@link UiObject#click()} only in that this method waits for a
+ * a new window transition as a result of the click. Some examples of a window transition:
+ * <li>launching a new activity</li>
+ * <li>bringing up a pop-up menu</li>
+ * <li>bringing up a dialog</li>
+ *
+ * @param timeout timeout before giving up on waiting for a new window
+ * @return true if the event was triggered, else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException {
+ Tracer.trace(timeout);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(),
+ mConfig.getActionAcknowledgmentTimeout());
+ }
+
+ /**
+ * Clicks the top and left corner of the UI element
+ *
+ * @return true on success
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean clickTopLeft() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().clickNoSync(rect.left + 5, rect.top + 5);
+ }
+
+ /**
+ * Long clicks bottom and right corner of the UI element
+ *
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean longClickBottomRight() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTapNoSync(rect.right - 5, rect.bottom - 5);
+ }
+
+ /**
+ * Clicks the bottom and right corner of the UI element
+ *
+ * @return true on success
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean clickBottomRight() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().clickNoSync(rect.right - 5, rect.bottom - 5);
+ }
+
+ /**
+ * Long clicks the center of the visible bounds of the UI element
+ *
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean longClick() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTapNoSync(rect.centerX(), rect.centerY());
+ }
+
+ /**
+ * Long clicks on the top and left corner of the UI element
+ *
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean longClickTopLeft() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTapNoSync(rect.left + 5, rect.top + 5);
+ }
+
+ /**
+ * Reads the <code>text</code> property of the UI element
+ *
+ * @return text value of the current node represented by this UiObject
+ * @throws UiObjectNotFoundException if no match could be found
+ * @since API Level 16
+ */
+ public String getText() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ String retVal = safeStringReturn(node.getText());
+ Log.d(LOG_TAG, String.format("getText() = %s", retVal));
+ return retVal;
+ }
+
+ /**
+ * Retrieves the <code>className</code> property of the UI element.
+ *
+ * @return class name of the current node represented by this UiObject
+ * @throws UiObjectNotFoundException if no match was found
+ * @since API Level 18
+ */
+ public String getClassName() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ String retVal = safeStringReturn(node.getClassName());
+ Log.d(LOG_TAG, String.format("getClassName() = %s", retVal));
+ return retVal;
+ }
+
+ /**
+ * Reads the <code>content_desc</code> property of the UI element
+ *
+ * @return value of node attribute "content_desc"
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public String getContentDescription() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return safeStringReturn(node.getContentDescription());
+ }
+
+ /**
+ * Sets the text in an editable field, after clearing the field's content.
+ *
+ * The {@link UiSelector} selector of this object must reference a UI element that is editable.
+ *
+ * When you call this method, the method first simulates a {@link #click()} on
+ * editable field to set focus. The method then clears the field's contents
+ * and injects your specified text into the field.
+ *
+ * If you want to capture the original contents of the field, call {@link #getText()} first.
+ * You can then modify the text and use this method to update the field.
+ *
+ * @param text string to set
+ * @return true if operation is successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean setText(String text) throws UiObjectNotFoundException {
+ Tracer.trace(text);
+ clearTextField();
+ return getInteractionController().sendText(text);
+ }
+
+ /**
+ * Clears the existing text contents in an editable field.
+ *
+ * The {@link UiSelector} of this object must reference a UI element that is editable.
+ *
+ * When you call this method, the method first sets focus at the start edge of the field.
+ * The method then simulates a long-press to select the existing text, and deletes the
+ * selected text.
+ *
+ * If a "Select-All" option is displayed, the method will automatically attempt to use it
+ * to ensure full text selection.
+ *
+ * Note that it is possible that not all the text in the field is selected; for example,
+ * if the text contains separators such as spaces, slashes, at symbol etc.
+ * Also, not all editable fields support the long-press functionality.
+ *
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public void clearTextField() throws UiObjectNotFoundException {
+ Tracer.trace();
+ // long click left + center
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ getInteractionController().longTapNoSync(rect.left + 20, rect.centerY());
+ // check if the edit menu is open
+ UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all"));
+ if(selectAll.waitForExists(50))
+ selectAll.click();
+ // wait for the selection
+ SystemClock.sleep(250);
+ // delete it
+ getInteractionController().sendKey(KeyEvent.KEYCODE_DEL, 0);
+ }
+
+ /**
+ * Check if the UI element's <code>checked</code> property is currently true
+ *
+ * @return true if it is else false
+ * @since API Level 16
+ */
+ public boolean isChecked() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isChecked();
+ }
+
+ /**
+ * Checks if the UI element's <code>selected</code> property is currently true.
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isSelected() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isSelected();
+ }
+
+ /**
+ * Checks if the UI element's <code>checkable</code> property is currently true.
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isCheckable() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isCheckable();
+ }
+
+ /**
+ * Checks if the UI element's <code>enabled</code> property is currently true.
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isEnabled() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isEnabled();
+ }
+
+ /**
+ * Checks if the UI element's <code>clickable</code> property is currently true.
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isClickable() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isClickable();
+ }
+
+ /**
+ * Check if the UI element's <code>focused</code> property is currently true
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isFocused() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isFocused();
+ }
+
+ /**
+ * Check if the UI element's <code>focusable</code> property is currently true.
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isFocusable() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isFocusable();
+ }
+
+ /**
+ * Check if the view's <code>scrollable</code> property is currently true
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isScrollable() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isScrollable();
+ }
+
+ /**
+ * Check if the view's <code>long-clickable</code> property is currently true
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public boolean isLongClickable() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isLongClickable();
+ }
+
+ /**
+ * Reads the view's <code>package</code> property
+ *
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public String getPackageName() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return safeStringReturn(node.getPackageName());
+ }
+
+ /**
+ * Returns the visible bounds of the view.
+ *
+ * If a portion of the view is visible, only the bounds of the visible portion are
+ * reported.
+ *
+ * @return Rect
+ * @throws UiObjectNotFoundException
+ * @see {@link #getBounds()}
+ * @since API Level 17
+ */
+ public Rect getVisibleBounds() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return getVisibleBounds(node);
+ }
+
+ /**
+ * Returns the view's <code>bounds</code> property. See {@link #getVisibleBounds()}
+ *
+ * @return Rect
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public Rect getBounds() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect nodeRect = new Rect();
+ node.getBoundsInScreen(nodeRect);
+
+ return nodeRect;
+ }
+
+ /**
+ * Waits a specified length of time for a view to become visible.
+ *
+ * This method waits until the view becomes visible on the display, or
+ * until the timeout has elapsed. You can use this method in situations where
+ * the content that you want to select is not immediately displayed.
+ *
+ * @param timeout the amount of time to wait (in milliseconds)
+ * @return true if the view is displayed, else false if timeout elapsed while waiting
+ * @since API Level 16
+ */
+ public boolean waitForExists(long timeout) {
+ Tracer.trace(timeout);
+ if(findAccessibilityNodeInfo(timeout) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Waits a specified length of time for a view to become undetectable.
+ *
+ * This method waits until a view is no longer matchable, or until the
+ * timeout has elapsed.
+ *
+ * A view becomes undetectable when the {@link UiSelector} of the object is
+ * unable to find a match because the element has either changed its state or is no
+ * longer displayed.
+ *
+ * You can use this method when attempting to wait for some long operation
+ * to compete, such as downloading a large file or connecting to a remote server.
+ *
+ * @param timeout time to wait (in milliseconds)
+ * @return true if the element is gone before timeout elapsed, else false if timeout elapsed
+ * but a matching element is still found.
+ * @since API Level 16
+ */
+ public boolean waitUntilGone(long timeout) {
+ Tracer.trace(timeout);
+ long startMills = SystemClock.uptimeMillis();
+ long currentMills = 0;
+ while (currentMills <= timeout) {
+ if(findAccessibilityNodeInfo(0) == null)
+ return true;
+ currentMills = SystemClock.uptimeMillis() - startMills;
+ if(timeout > 0)
+ SystemClock.sleep(WAIT_FOR_SELECTOR_POLL);
+ }
+ return false;
+ }
+
+ /**
+ * Check if view exists.
+ *
+ * This methods performs a {@link #waitForExists(long)} with zero timeout. This
+ * basically returns immediately whether the view represented by this UiObject
+ * exists or not. If you need to wait longer for this view, then see
+ * {@link #waitForExists(long)}.
+ *
+ * @return true if the view represented by this UiObject does exist
+ * @since API Level 16
+ */
+ public boolean exists() {
+ Tracer.trace();
+ return waitForExists(0);
+ }
+
+ private String safeStringReturn(CharSequence cs) {
+ if(cs == null)
+ return "";
+ return cs.toString();
+ }
+
+ /**
+ * Performs a two-pointer gesture, where each pointer moves diagonally
+ * opposite across the other, from the center out towards the edges of the
+ * this UiObject.
+ * @param percent percentage of the object's diagonal length for the pinch gesture
+ * @param steps the number of steps for the gesture. Steps are injected
+ * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
+ * @return <code>true</code> if all touch events for this gesture are injected successfully,
+ * <code>false</code> otherwise
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException {
+ // make value between 1 and 100
+ percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+ float percentage = percent / 100f;
+
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if (node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+
+ Rect rect = getVisibleBounds(node);
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
+ throw new IllegalStateException("Object width is too small for operation");
+
+ // start from the same point at the center of the control
+ Point startPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+ Point startPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+
+ // End at the top-left and bottom-right corners of the control
+ Point endPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
+ rect.centerY());
+ Point endPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
+ rect.centerY());
+
+ return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ /**
+ * Performs a two-pointer gesture, where each pointer moves diagonally
+ * toward the other, from the edges to the center of this UiObject .
+ * @param percent percentage of the object's diagonal length for the pinch gesture
+ * @param steps the number of steps for the gesture. Steps are injected
+ * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
+ * @return <code>true</code> if all touch events for this gesture are injected successfully,
+ * <code>false</code> otherwise
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException {
+ // make value between 1 and 100
+ percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent;
+ float percentage = percent / 100f;
+
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if (node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+
+ Rect rect = getVisibleBounds(node);
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
+ throw new IllegalStateException("Object width is too small for operation");
+
+ Point startPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
+ rect.centerY());
+ Point startPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
+ rect.centerY());
+
+ Point endPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+ Point endPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+
+ return performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ /**
+ * Generates a two-pointer gesture with arbitrary starting and ending points.
+ *
+ * @param startPoint1 start point of pointer 1
+ * @param startPoint2 start point of pointer 2
+ * @param endPoint1 end point of pointer 1
+ * @param endPoint2 end point of pointer 2
+ * @param steps the number of steps for the gesture. Steps are injected
+ * about 5 milliseconds apart, so 100 steps may take around 0.5 seconds to complete.
+ * @return <code>true</code> if all touch events for this gesture are injected successfully,
+ * <code>false</code> otherwise
+ * @since API Level 18
+ */
+ public boolean performTwoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1,
+ Point endPoint2, int steps) {
+
+ // avoid a divide by zero
+ if(steps == 0)
+ steps = 1;
+
+ final float stepX1 = (endPoint1.x - startPoint1.x) / steps;
+ final float stepY1 = (endPoint1.y - startPoint1.y) / steps;
+ final float stepX2 = (endPoint2.x - startPoint2.x) / steps;
+ final float stepY2 = (endPoint2.y - startPoint2.y) / steps;
+
+ int eventX1, eventY1, eventX2, eventY2;
+ eventX1 = startPoint1.x;
+ eventY1 = startPoint1.y;
+ eventX2 = startPoint2.x;
+ eventY2 = startPoint2.y;
+
+ // allocate for steps plus first down and last up
+ PointerCoords[] points1 = new PointerCoords[steps + 2];
+ PointerCoords[] points2 = new PointerCoords[steps + 2];
+
+ // Include the first and last touch downs in the arrays of steps
+ for (int i = 0; i < steps + 1; i++) {
+ PointerCoords p1 = new PointerCoords();
+ p1.x = eventX1;
+ p1.y = eventY1;
+ p1.pressure = 1;
+ p1.size = 1;
+ points1[i] = p1;
+
+ PointerCoords p2 = new PointerCoords();
+ p2.x = eventX2;
+ p2.y = eventY2;
+ p2.pressure = 1;
+ p2.size = 1;
+ points2[i] = p2;
+
+ eventX1 += stepX1;
+ eventY1 += stepY1;
+ eventX2 += stepX2;
+ eventY2 += stepY2;
+ }
+
+ // ending pointers coordinates
+ PointerCoords p1 = new PointerCoords();
+ p1.x = endPoint1.x;
+ p1.y = endPoint1.y;
+ p1.pressure = 1;
+ p1.size = 1;
+ points1[steps + 1] = p1;
+
+ PointerCoords p2 = new PointerCoords();
+ p2.x = endPoint2.x;
+ p2.y = endPoint2.y;
+ p2.pressure = 1;
+ p2.size = 1;
+ points2[steps + 1] = p2;
+
+ return performMultiPointerGesture(points1, points2);
+ }
+
+ /**
+ * Performs a multi-touch gesture. You must specify touch coordinates for
+ * at least 2 pointers. Each pointer must have all of its touch steps
+ * defined in an array of {@link PointerCoords}. You can use this method to
+ * specify complex gestures, like circles and irregular shapes, where each
+ * pointer may take a different path.
+ *
+ * To create a single point on a pointer's touch path:
+ * <code>
+ * PointerCoords p = new PointerCoords();
+ * p.x = stepX;
+ * p.y = stepY;
+ * p.pressure = 1;
+ * p.size = 1;
+ * </code>
+ * @param touches represents the pointers' paths. Each {@link PointerCoords}
+ * array represents a different pointer. Each {@link PointerCoords} in an
+ * array element represents a touch point on a pointer's path.
+ * @return <code>true</code> if all touch events for this gesture are injected successfully,
+ * <code>false</code> otherwise
+ * @since API Level 18
+ */
+ public boolean performMultiPointerGesture(PointerCoords[] ...touches) {
+ return getInteractionController().performMultiPointerGesture(touches);
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java
new file mode 100644
index 0000000..fc0891b
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Generated in test runs when a {@link UiSelector} selector could not be matched
+ * to any UI element displayed.
+ * @since API Level 16
+ */
+public class UiObjectNotFoundException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @since API Level 16
+ **/
+ public UiObjectNotFoundException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * @since API Level 16
+ **/
+ public UiObjectNotFoundException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ /**
+ * @since API Level 16
+ **/
+ public UiObjectNotFoundException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java
new file mode 100644
index 0000000..a8d20c3
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java
@@ -0,0 +1,665 @@
+/*
+ * 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.graphics.Rect;
+import android.util.Log;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * UiScrollable is a {@link UiCollection} and provides support for searching
+ * for items in scrollable layout elements. This class can be used with
+ * horizontally or vertically scrollable controls.
+ * @since API Level 16
+ */
+public class UiScrollable extends UiCollection {
+ private static final String LOG_TAG = UiScrollable.class.getSimpleName();
+
+ // More steps slows the swipe and prevents contents from being flung too far
+ private static final int SCROLL_STEPS = 55;
+
+ private static final int FLING_STEPS = 5;
+
+ // Restrict a swipe's starting and ending points inside a 10% margin of the target
+ private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1;
+
+ // Limits the number of swipes/scrolls performed during a search
+ private static int mMaxSearchSwipes = 30;
+
+ // Used in ScrollForward() and ScrollBackward() to determine swipe direction
+ private boolean mIsVerticalList = true;
+
+ private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
+
+ /**
+ * Constructor.
+ *
+ * @param container a {@link UiSelector} selector to identify the scrollable
+ * layout element.
+ * @since API Level 16
+ */
+ public UiScrollable(UiSelector container) {
+ // wrap the container selector with container so that QueryController can handle
+ // this type of enumeration search accordingly
+ super(container);
+ }
+
+ /**
+ * Set the direction of swipes to be vertical when performing scroll actions.
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setAsVerticalList() {
+ Tracer.trace();
+ mIsVerticalList = true;
+ return this;
+ }
+
+ /**
+ * Set the direction of swipes to be horizontal when performing scroll actions.
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setAsHorizontalList() {
+ Tracer.trace();
+ mIsVerticalList = false;
+ return this;
+ }
+
+ /**
+ * Used privately when performing swipe searches to decide if an element has become
+ * visible or not.
+ *
+ * @param selector
+ * @return true if found else false
+ * @since API Level 16
+ */
+ protected boolean exists(UiSelector selector) {
+ if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Searches for a child element in the present scrollable container.
+ * The search first looks for a child element that matches the selector
+ * you provided, then looks for the content-description in its children elements.
+ * If both search conditions are fulfilled, the method returns a {@ link UiObject}
+ * representing the element matching the selector (not the child element in its
+ * subhierarchy containing the content-description). By default, this method performs a
+ * scroll search.
+ * See {@link #getChildByDescription(UiSelector, String, boolean)}
+ *
+ * @param childPattern {@link UiSelector} for a child in a scollable layout element
+ * @param text Content-description to find in the children of
+ * the <code>childPattern</code> match
+ * @return {@link UiObject} representing the child element that matches the search conditions
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ @Override
+ public UiObject getChildByDescription(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ return getChildByDescription(childPattern, text, true);
+ }
+
+ /**
+ * Searches for a child element in the present scrollable container.
+ * The search first looks for a child element that matches the selector
+ * you provided, then looks for the content-description in its children elements.
+ * If both search conditions are fulfilled, the method returns a {@ link UiObject}
+ * representing the element matching the selector (not the child element in its
+ * subhierarchy containing the content-description).
+ *
+ * @param childPattern {@link UiSelector} for a child in a scollable layout element
+ * @param text Content-description to find in the children of
+ * the <code>childPattern</code> match (may be a partial match)
+ * @param allowScrollSearch set to true if scrolling is allowed
+ * @return {@link UiObject} representing the child element that matches the search conditions
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public UiObject getChildByDescription(UiSelector childPattern, String text,
+ boolean allowScrollSearch) throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text, allowScrollSearch);
+ if (text != null) {
+ if (allowScrollSearch) {
+ scrollIntoView(new UiSelector().descriptionContains(text));
+ }
+ return super.getChildByDescription(childPattern, text);
+ }
+ throw new UiObjectNotFoundException("for description= \"" + text + "\"");
+ }
+
+ /**
+ * Searches for a child element in the present scrollable container that
+ * matches the selector you provided. The search is performed without
+ * scrolling and only on visible elements.
+ *
+ * @param childPattern {@link UiSelector} for a child in a scollable layout element
+ * @param instance int number representing the occurance of
+ * a <code>childPattern</code> match
+ * @return {@link UiObject} representing the child element that matches the search conditions
+ * @since API Level 16
+ */
+ @Override
+ public UiObject getChildByInstance(UiSelector childPattern, int instance)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, instance);
+ UiSelector patternSelector = UiSelector.patternBuilder(getSelector(),
+ UiSelector.patternBuilder(childPattern).instance(instance));
+ return new UiObject(patternSelector);
+ }
+
+ /**
+ * Searches for a child element in the present scrollable
+ * container. The search first looks for a child element that matches the
+ * selector you provided, then looks for the text in its children elements.
+ * If both search conditions are fulfilled, the method returns a {@ link UiObject}
+ * representing the element matching the selector (not the child element in its
+ * subhierarchy containing the text). By default, this method performs a
+ * scroll search.
+ * See {@link #getChildByText(UiSelector, String, boolean)}
+ *
+ * @param childPattern {@link UiSelector} selector for a child in a scrollable layout element
+ * @param text String to find in the children of the <code>childPattern</code> match
+ * @return {@link UiObject} representing the child element that matches the search conditions
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ @Override
+ public UiObject getChildByText(UiSelector childPattern, String text)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text);
+ return getChildByText(childPattern, text, true);
+ }
+
+ /**
+ * Searches for a child element in the present scrollable container. The
+ * search first looks for a child element that matches the
+ * selector you provided, then looks for the text in its children elements.
+ * If both search conditions are fulfilled, the method returns a {@ link UiObject}
+ * representing the element matching the selector (not the child element in its
+ * subhierarchy containing the text).
+ *
+ * @param childPattern {@link UiSelector} selector for a child in a scrollable layout element
+ * @param text String to find in the children of the <code>childPattern</code> match
+ * @param allowScrollSearch set to true if scrolling is allowed
+ * @return {@link UiObject} representing the child element that matches the search conditions
+ * @throws UiObjectNotFoundException
+ * @since API Level 16
+ */
+ public UiObject getChildByText(UiSelector childPattern, String text, boolean allowScrollSearch)
+ throws UiObjectNotFoundException {
+ Tracer.trace(childPattern, text, allowScrollSearch);
+ if (text != null) {
+ if (allowScrollSearch) {
+ scrollIntoView(new UiSelector().text(text));
+ }
+ return super.getChildByText(childPattern, text);
+ }
+ throw new UiObjectNotFoundException("for text= \"" + text + "\"");
+ }
+
+ /**
+ * Performs a forward scroll action on the scrollable layout element until
+ * the content-description is found, or until swipe attempts have been exhausted.
+ * See {@link #setMaxSearchSwipes(int)}
+ *
+ * @param text content-description to find within the contents of this scrollable layout element.
+ * @return true if item is found; else, false
+ * @since API Level 16
+ */
+ public boolean scrollDescriptionIntoView(String text) throws UiObjectNotFoundException {
+ Tracer.trace(text);
+ return scrollIntoView(new UiSelector().description(text));
+ }
+
+ /**
+ * Perform a forward scroll action to move through the scrollable layout element until
+ * a visible item that matches the {@link UiObject} is found.
+ *
+ * @param obj {@link UiObject}
+ * @return true if the item was found and now is in view else false
+ * @since API Level 16
+ */
+ public boolean scrollIntoView(UiObject obj) throws UiObjectNotFoundException {
+ Tracer.trace(obj.getSelector());
+ return scrollIntoView(obj.getSelector());
+ }
+
+ /**
+ * Perform a scroll forward action to move through the scrollable layout
+ * element until a visible item that matches the selector is found.
+ *
+ * See {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
+ *
+ * @param selector {@link UiSelector} selector
+ * @return true if the item was found and now is in view; else, false
+ * @since API Level 16
+ */
+ public boolean scrollIntoView(UiSelector selector) throws UiObjectNotFoundException {
+ Tracer.trace(selector);
+ // if we happen to be on top of the text we want then return here
+ UiSelector childSelector = getSelector().childSelector(selector);
+ if (exists(childSelector)) {
+ return (true);
+ } else {
+ // we will need to reset the search from the beginning to start search
+ scrollToBeginning(mMaxSearchSwipes);
+ if (exists(childSelector)) {
+ return (true);
+ }
+ for (int x = 0; x < mMaxSearchSwipes; x++) {
+ boolean scrolled = scrollForward();
+ if(exists(childSelector)) {
+ return true;
+ }
+ if (!scrolled) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Scrolls forward until the UiObject is fully visible in the scrollable container.
+ * Use this method to make sure that the child item's edges are not offscreen.
+ *
+ * @param childObject {@link UiObject} representing the child element
+ * @return true if the child element is already fully visible, or
+ * if the method scrolled successfully until the child became fully visible;
+ * otherwise, false if the attempt to scroll failed.
+ * @throws UiObjectNotFoundException
+ * @hide
+ */
+ public boolean ensureFullyVisible(UiObject childObject) throws UiObjectNotFoundException {
+ Rect actual = childObject.getBounds();
+ Rect visible = childObject.getVisibleBounds();
+ if (visible.width() * visible.height() == actual.width() * actual.height()) {
+ // area match, item fully visible
+ return true;
+ }
+ boolean shouldSwipeForward = false;
+ if (mIsVerticalList) {
+ // if list is vertical, matching top edge implies obscured bottom edge
+ // so we need to scroll list forward
+ shouldSwipeForward = actual.top == visible.top;
+ } else {
+ // if list is horizontal, matching left edge implies obscured right edge,
+ // so we need to scroll list forward
+ shouldSwipeForward = actual.left == visible.left;
+ }
+ if (mIsVerticalList) {
+ if (shouldSwipeForward) {
+ return swipeUp(10);
+ } else {
+ return swipeDown(10);
+ }
+ } else {
+ if (shouldSwipeForward) {
+ return swipeLeft(10);
+ } else {
+ return swipeRight(10);
+ }
+ }
+ }
+
+ /**
+ * Performs a forward scroll action on the scrollable layout element until
+ * the text you provided is visible, or until swipe attempts have been exhausted.
+ * See {@link #setMaxSearchSwipes(int)}
+ *
+ * @param text test to look for
+ * @return true if item is found; else, false
+ * @since API Level 16
+ */
+ public boolean scrollTextIntoView(String text) throws UiObjectNotFoundException {
+ Tracer.trace(text);
+ return scrollIntoView(new UiSelector().text(text));
+ }
+
+ /**
+ * Sets the maximum number of scrolls allowed when performing a
+ * scroll action in search of a child element.
+ * See {@link #getChildByDescription(UiSelector, String)} and
+ * {@link #getChildByText(UiSelector, String)}.
+ *
+ * @param swipes the number of search swipes to perform until giving up
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setMaxSearchSwipes(int swipes) {
+ Tracer.trace(swipes);
+ mMaxSearchSwipes = swipes;
+ return this;
+ }
+
+ /**
+ * Gets the maximum number of scrolls allowed when performing a
+ * scroll action in search of a child element.
+ * See {@link #getChildByDescription(UiSelector, String)} and
+ * {@link #getChildByText(UiSelector, String)}.
+ *
+ * @return max the number of search swipes to perform until giving up
+ * @since API Level 16
+ */
+ public int getMaxSearchSwipes() {
+ Tracer.trace();
+ return mMaxSearchSwipes;
+ }
+
+ /**
+ * Performs a forward fling with the default number of fling steps (5).
+ * If the swipe direction is set to vertical, then the swipes will be
+ * performed from bottom to top. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * right to left. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @return true if scrolled, false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean flingForward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ return scrollForward(FLING_STEPS);
+ }
+
+ /**
+ * Performs a forward scroll with the default number of scroll steps (55).
+ * If the swipe direction is set to vertical,
+ * then the swipes will be performed from bottom to top. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * right to left. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @return true if scrolled, false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean scrollForward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ return scrollForward(SCROLL_STEPS);
+ }
+
+ /**
+ * Performs a forward scroll. If the swipe direction is set to vertical,
+ * then the swipes will be performed from bottom to top. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * right to left. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @param steps number of steps. Use this to control the speed of the scroll action
+ * @return true if scrolled, false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean scrollForward(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = new Rect();
+ node.getBoundsInScreen(rect);
+
+ int downX = 0;
+ int downY = 0;
+ int upX = 0;
+ int upY = 0;
+
+ // scrolling is by default assumed vertically unless the object is explicitly
+ // set otherwise by setAsHorizontalContainer()
+ if(mIsVerticalList) {
+ int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
+ // scroll vertically: swipe down -> up
+ downX = rect.centerX();
+ downY = rect.bottom - swipeAreaAdjust;
+ upX = rect.centerX();
+ upY = rect.top + swipeAreaAdjust;
+ } else {
+ int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
+ // scroll horizontally: swipe right -> left
+ // TODO: Assuming device is not in right to left language
+ downX = rect.right - swipeAreaAdjust;
+ downY = rect.centerY();
+ upX = rect.left + swipeAreaAdjust;
+ upY = rect.centerY();
+ }
+ return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
+ }
+
+ /**
+ * Performs a backwards fling action with the default number of fling
+ * steps (5). If the swipe direction is set to vertical,
+ * then the swipe will be performed from top to bottom. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * left to right. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @return true if scrolled, and false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean flingBackward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ return scrollBackward(FLING_STEPS);
+ }
+
+ /**
+ * Performs a backward scroll with the default number of scroll steps (55).
+ * If the swipe direction is set to vertical,
+ * then the swipes will be performed from top to bottom. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * left to right. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @return true if scrolled, and false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean scrollBackward() throws UiObjectNotFoundException {
+ Tracer.trace();
+ return scrollBackward(SCROLL_STEPS);
+ }
+
+ /**
+ * Performs a backward scroll. If the swipe direction is set to vertical,
+ * then the swipes will be performed from top to bottom. If the swipe
+ * direction is set to horizontal, then the swipes will be performed from
+ * left to right. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @param steps number of steps. Use this to control the speed of the scroll action.
+ * @return true if scrolled, false if can't scroll anymore
+ * @since API Level 16
+ */
+ public boolean scrollBackward(int steps) throws UiObjectNotFoundException {
+ Tracer.trace(steps);
+ Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if (node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = new Rect();
+ node.getBoundsInScreen(rect);
+
+ int downX = 0;
+ int downY = 0;
+ int upX = 0;
+ int upY = 0;
+
+ // scrolling is by default assumed vertically unless the object is explicitly
+ // set otherwise by setAsHorizontalContainer()
+ if(mIsVerticalList) {
+ int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage());
+ Log.d(LOG_TAG, "scrollToBegining() using vertical scroll");
+ // scroll vertically: swipe up -> down
+ downX = rect.centerX();
+ downY = rect.top + swipeAreaAdjust;
+ upX = rect.centerX();
+ upY = rect.bottom - swipeAreaAdjust;
+ } else {
+ int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage());
+ Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll");
+ // scroll horizontally: swipe left -> right
+ // TODO: Assuming device is not in right to left language
+ downX = rect.left + swipeAreaAdjust;
+ downY = rect.centerY();
+ upX = rect.right - swipeAreaAdjust;
+ upY = rect.centerY();
+ }
+ return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps);
+ }
+
+ /**
+ * Scrolls to the beginning of a scrollable layout element. The beginning
+ * can be at the top-most edge in the case of vertical controls, or the
+ * left-most edge for horizontal controls. Make sure to take into account
+ * devices configured with right-to-left languages like Arabic and Hebrew.
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true on scrolled else false
+ * @since API Level 16
+ */
+ public boolean scrollToBeginning(int maxSwipes, int steps) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes, steps);
+ Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector());
+ // protect against potential hanging and return after preset attempts
+ for(int x = 0; x < maxSwipes; x++) {
+ if(!scrollBackward(steps)) {
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Scrolls to the beginning of a scrollable layout element. The beginning
+ * can be at the top-most edge in the case of vertical controls, or the
+ * left-most edge for horizontal controls. Make sure to take into account
+ * devices configured with right-to-left languages like Arabic and Hebrew.
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ * @since API Level 16
+ */
+ public boolean scrollToBeginning(int maxSwipes) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes);
+ return scrollToBeginning(maxSwipes, SCROLL_STEPS);
+ }
+
+ /**
+ * Performs a fling gesture to reach the beginning of a scrollable layout element.
+ * The beginning can be at the top-most edge in the case of vertical controls, or
+ * the left-most edge for horizontal controls. Make sure to take into
+ * account devices configured with right-to-left languages like Arabic and Hebrew.
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ * @since API Level 16
+ */
+ public boolean flingToBeginning(int maxSwipes) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes);
+ return scrollToBeginning(maxSwipes, FLING_STEPS);
+ }
+
+ /**
+ * Scrolls to the end of a scrollable layout element. The end can be at the
+ * bottom-most edge in the case of vertical controls, or the right-most edge for
+ * horizontal controls. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true on scrolled else false
+ * @since API Level 16
+ */
+ public boolean scrollToEnd(int maxSwipes, int steps) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes, steps);
+ // protect against potential hanging and return after preset attempts
+ for(int x = 0; x < maxSwipes; x++) {
+ if(!scrollForward(steps)) {
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Scrolls to the end of a scrollable layout element. The end can be at the
+ * bottom-most edge in the case of vertical controls, or the right-most edge for
+ * horizontal controls. Make sure to take into account devices configured with
+ * right-to-left languages like Arabic and Hebrew.
+ *
+ * @param maxSwipes
+ * @return true on scrolled, else false
+ * @since API Level 16
+ */
+ public boolean scrollToEnd(int maxSwipes) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes);
+ return scrollToEnd(maxSwipes, SCROLL_STEPS);
+ }
+
+ /**
+ * Performs a fling gesture to reach the end of a scrollable layout element.
+ * The end can be at the bottom-most edge in the case of vertical controls, or
+ * the right-most edge for horizontal controls. Make sure to take into
+ * account devices configured with right-to-left languages like Arabic and Hebrew.
+ *
+ * @param maxSwipes
+ * @return true on scrolled, else false
+ * @since API Level 16
+ */
+ public boolean flingToEnd(int maxSwipes) throws UiObjectNotFoundException {
+ Tracer.trace(maxSwipes);
+ return scrollToEnd(maxSwipes, FLING_STEPS);
+ }
+
+ /**
+ * Returns the percentage of a widget's size that's considered as a no-touch
+ * zone when swiping. The no-touch zone is set as a percentage of a widget's total
+ * width or height, denoting a margin around the swipable area of the widget.
+ * Swipes must start and end inside this margin. This is important when the
+ * widget being swiped may not respond to the swipe if started at a point
+ * too near to the edge. The default is 10% from either edge.
+ *
+ * @return a value between 0 and 1
+ * @since API Level 16
+ */
+ public double getSwipeDeadZonePercentage() {
+ Tracer.trace();
+ return mSwipeDeadZonePercentage;
+ }
+
+ /**
+ * Sets the percentage of a widget's size that's considered as no-touch
+ * zone when swiping.
+ * The no-touch zone is set as percentage of a widget's total width or height,
+ * denoting a margin around the swipable area of the widget. Swipes must
+ * always start and end inside this margin. This is important when the
+ * widget being swiped may not respond to the swipe if started at a point
+ * too near to the edge. The default is 10% from either edge.
+ *
+ * @param swipeDeadZonePercentage is a value between 0 and 1
+ * @return reference to itself
+ * @since API Level 16
+ */
+ public UiScrollable setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
+ Tracer.trace(swipeDeadZonePercentage);
+ mSwipeDeadZonePercentage = swipeDeadZonePercentage;
+ return this;
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
new file mode 100644
index 0000000..482a74d
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
@@ -0,0 +1,1022 @@
+/*
+ * 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.util.SparseArray;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.regex.Pattern;
+
+/**
+ * Specifies the elements in the layout hierarchy for tests to target, filtered
+ * by properties such as text value, content-description, class name, and state
+ * information. You can also target an element by its location in a layout
+ * hierarchy.
+ * @since API Level 16
+ */
+public class UiSelector {
+ static final int SELECTOR_NIL = 0;
+ static final int SELECTOR_TEXT = 1;
+ static final int SELECTOR_START_TEXT = 2;
+ static final int SELECTOR_CONTAINS_TEXT = 3;
+ static final int SELECTOR_CLASS = 4;
+ static final int SELECTOR_DESCRIPTION = 5;
+ static final int SELECTOR_START_DESCRIPTION = 6;
+ static final int SELECTOR_CONTAINS_DESCRIPTION = 7;
+ static final int SELECTOR_INDEX = 8;
+ static final int SELECTOR_INSTANCE = 9;
+ static final int SELECTOR_ENABLED = 10;
+ static final int SELECTOR_FOCUSED = 11;
+ static final int SELECTOR_FOCUSABLE = 12;
+ static final int SELECTOR_SCROLLABLE = 13;
+ static final int SELECTOR_CLICKABLE = 14;
+ static final int SELECTOR_CHECKED = 15;
+ static final int SELECTOR_SELECTED = 16;
+ static final int SELECTOR_ID = 17;
+ static final int SELECTOR_PACKAGE_NAME = 18;
+ static final int SELECTOR_CHILD = 19;
+ static final int SELECTOR_CONTAINER = 20;
+ static final int SELECTOR_PATTERN = 21;
+ static final int SELECTOR_PARENT = 22;
+ static final int SELECTOR_COUNT = 23;
+ static final int SELECTOR_LONG_CLICKABLE = 24;
+ static final int SELECTOR_TEXT_REGEX = 25;
+ static final int SELECTOR_CLASS_REGEX = 26;
+ static final int SELECTOR_DESCRIPTION_REGEX = 27;
+ static final int SELECTOR_PACKAGE_NAME_REGEX = 28;
+ static final int SELECTOR_RESOURCE_ID = 29;
+ static final int SELECTOR_CHECKABLE = 30;
+ static final int SELECTOR_RESOURCE_ID_REGEX = 31;
+
+ private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>();
+
+ /**
+ * @since API Level 16
+ */
+ public UiSelector() {
+ }
+
+ UiSelector(UiSelector selector) {
+ mSelectorAttributes = selector.cloneSelector().mSelectorAttributes;
+ }
+
+ /**
+ * @since API Level 17
+ */
+ protected UiSelector cloneSelector() {
+ UiSelector ret = new UiSelector();
+ ret.mSelectorAttributes = mSelectorAttributes.clone();
+ if (hasChildSelector())
+ ret.mSelectorAttributes.put(SELECTOR_CHILD, new UiSelector(getChildSelector()));
+ if (hasParentSelector())
+ ret.mSelectorAttributes.put(SELECTOR_PARENT, new UiSelector(getParentSelector()));
+ if (hasPatternSelector())
+ ret.mSelectorAttributes.put(SELECTOR_PATTERN, new UiSelector(getPatternSelector()));
+ return ret;
+ }
+
+ static UiSelector patternBuilder(UiSelector selector) {
+ if (!selector.hasPatternSelector()) {
+ return new UiSelector().patternSelector(selector);
+ }
+ return selector;
+ }
+
+ static UiSelector patternBuilder(UiSelector container, UiSelector pattern) {
+ return new UiSelector(
+ new UiSelector().containerSelector(container).patternSelector(pattern));
+ }
+
+ /**
+ * Set the search criteria to match the visible text displayed
+ * in a widget (for example, the text label to launch an app).
+ *
+ * The text for the element must match exactly with the string in your input
+ * argument. Matching is case-sensitive.
+ *
+ * @param text Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector text(String text) {
+ return buildSelector(SELECTOR_TEXT, text);
+ }
+
+ /**
+ * Set the search criteria to match the visible text displayed in a layout
+ * element, using a regular expression.
+ *
+ * The text in the widget must match exactly with the string in your
+ * input argument.
+ *
+ * @param regex a regular expression
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public UiSelector textMatches(String regex) {
+ return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex));
+ }
+
+ /**
+ * Set the search criteria to match visible text in a widget that is
+ * prefixed by the text parameter.
+ *
+ * The matching is case-insensitive.
+ *
+ * @param text Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector textStartsWith(String text) {
+ return buildSelector(SELECTOR_START_TEXT, text);
+ }
+
+ /**
+ * Set the search criteria to match the visible text in a widget
+ * where the visible text must contain the string in your input argument.
+ *
+ * The matching is case-sensitive.
+ *
+ * @param text Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector textContains(String text) {
+ return buildSelector(SELECTOR_CONTAINS_TEXT, text);
+ }
+
+ /**
+ * Set the search criteria to match the class property
+ * for a widget (for example, "android.widget.Button").
+ *
+ * @param className Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector className(String className) {
+ return buildSelector(SELECTOR_CLASS, className);
+ }
+
+ /**
+ * Set the search criteria to match the class property
+ * for a widget, using a regular expression.
+ *
+ * @param regex a regular expression
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public UiSelector classNameMatches(String regex) {
+ return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
+ }
+
+ /**
+ * Set the search criteria to match the class property
+ * for a widget (for example, "android.widget.Button").
+ *
+ * @param type type
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public <T> UiSelector className(Class<T> type) {
+ return buildSelector(SELECTOR_CLASS, type.getName());
+ }
+
+ /**
+ * Set the search criteria to match the content-description
+ * property for a widget.
+ *
+ * The content-description is typically used
+ * by the Android Accessibility framework to
+ * provide an audio prompt for the widget when
+ * the widget is selected. The content-description
+ * for the widget must match exactly
+ * with the string in your input argument.
+ *
+ * Matching is case-sensitive.
+ *
+ * @param desc Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector description(String desc) {
+ return buildSelector(SELECTOR_DESCRIPTION, desc);
+ }
+
+ /**
+ * Set the search criteria to match the content-description
+ * property for a widget.
+ *
+ * The content-description is typically used
+ * by the Android Accessibility framework to
+ * provide an audio prompt for the widget when
+ * the widget is selected. The content-description
+ * for the widget must match exactly
+ * with the string in your input argument.
+ *
+ * @param regex a regular expression
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public UiSelector descriptionMatches(String regex) {
+ return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex));
+ }
+
+ /**
+ * Set the search criteria to match the content-description
+ * property for a widget.
+ *
+ * The content-description is typically used
+ * by the Android Accessibility framework to
+ * provide an audio prompt for the widget when
+ * the widget is selected. The content-description
+ * for the widget must start
+ * with the string in your input argument.
+ *
+ * Matching is case-insensitive.
+ *
+ * @param desc Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector descriptionStartsWith(String desc) {
+ return buildSelector(SELECTOR_START_DESCRIPTION, desc);
+ }
+
+ /**
+ * Set the search criteria to match the content-description
+ * property for a widget.
+ *
+ * The content-description is typically used
+ * by the Android Accessibility framework to
+ * provide an audio prompt for the widget when
+ * the widget is selected. The content-description
+ * for the widget must contain
+ * the string in your input argument.
+ *
+ * Matching is case-insensitive.
+ *
+ * @param desc Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector descriptionContains(String desc) {
+ return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
+ }
+
+ /**
+ * Set the search criteria to match the given resource ID.
+ *
+ * @param id Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 18
+ */
+ public UiSelector resourceId(String id) {
+ return buildSelector(SELECTOR_RESOURCE_ID, id);
+ }
+
+ /**
+ * Set the search criteria to match the resource ID
+ * of the widget, using a regular expression.
+ *
+ * @param regex a regular expression
+ * @return UiSelector with the specified search criteria
+ * @since API Level 18
+ */
+ public UiSelector resourceIdMatches(String regex) {
+ return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
+ }
+
+ /**
+ * Set the search criteria to match the widget by its node
+ * index in the layout hierarchy.
+ *
+ * The index value must be 0 or greater.
+ *
+ * Using the index can be unreliable and should only
+ * be used as a last resort for matching. Instead,
+ * consider using the {@link #instance(int)} method.
+ *
+ * @param index Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector index(final int index) {
+ return buildSelector(SELECTOR_INDEX, index);
+ }
+
+ /**
+ * Set the search criteria to match the
+ * widget by its instance number.
+ *
+ * The instance value must be 0 or greater, where
+ * the first instance is 0.
+ *
+ * For example, to simulate a user click on
+ * the third image that is enabled in a UI screen, you
+ * could specify a a search criteria where the instance is
+ * 2, the {@link #className(String)} matches the image
+ * widget class, and {@link #enabled(boolean)} is true.
+ * The code would look like this:
+ * <code>
+ * new UiSelector().className("android.widget.ImageView")
+ * .enabled(true).instance(2);
+ * </code>
+ *
+ * @param instance Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector instance(final int instance) {
+ return buildSelector(SELECTOR_INSTANCE, instance);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are enabled.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector enabled(boolean val) {
+ return buildSelector(SELECTOR_ENABLED, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that have focus.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector focused(boolean val) {
+ return buildSelector(SELECTOR_FOCUSED, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are focusable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector focusable(boolean val) {
+ return buildSelector(SELECTOR_FOCUSABLE, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are scrollable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector scrollable(boolean val) {
+ return buildSelector(SELECTOR_SCROLLABLE, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that
+ * are currently selected.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector selected(boolean val) {
+ return buildSelector(SELECTOR_SELECTED, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that
+ * are currently checked (usually for checkboxes).
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector checked(boolean val) {
+ return buildSelector(SELECTOR_CHECKED, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are clickable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector clickable(boolean val) {
+ return buildSelector(SELECTOR_CLICKABLE, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are checkable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 18
+ */
+ public UiSelector checkable(boolean val) {
+ return buildSelector(SELECTOR_CHECKABLE, val);
+ }
+
+ /**
+ * Set the search criteria to match widgets that are long-clickable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public UiSelector longClickable(boolean val) {
+ return buildSelector(SELECTOR_LONG_CLICKABLE, val);
+ }
+
+ /**
+ * Adds a child UiSelector criteria to this selector.
+ *
+ * Use this selector to narrow the search scope to
+ * child widgets under a specific parent widget.
+ *
+ * @param selector
+ * @return UiSelector with this added search criterion
+ * @since API Level 16
+ */
+ public UiSelector childSelector(UiSelector selector) {
+ return buildSelector(SELECTOR_CHILD, selector);
+ }
+
+ private UiSelector patternSelector(UiSelector selector) {
+ return buildSelector(SELECTOR_PATTERN, selector);
+ }
+
+ private UiSelector containerSelector(UiSelector selector) {
+ return buildSelector(SELECTOR_CONTAINER, selector);
+ }
+
+ /**
+ * Adds a child UiSelector criteria to this selector which is used to
+ * start search from the parent widget.
+ *
+ * Use this selector to narrow the search scope to
+ * sibling widgets as well all child widgets under a parent.
+ *
+ * @param selector
+ * @return UiSelector with this added search criterion
+ * @since API Level 16
+ */
+ public UiSelector fromParent(UiSelector selector) {
+ return buildSelector(SELECTOR_PARENT, selector);
+ }
+
+ /**
+ * Set the search criteria to match the package name
+ * of the application that contains the widget.
+ *
+ * @param name Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 16
+ */
+ public UiSelector packageName(String name) {
+ return buildSelector(SELECTOR_PACKAGE_NAME, name);
+ }
+
+ /**
+ * Set the search criteria to match the package name
+ * of the application that contains the widget.
+ *
+ * @param regex a regular expression
+ * @return UiSelector with the specified search criteria
+ * @since API Level 17
+ */
+ public UiSelector packageNameMatches(String regex) {
+ return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
+ }
+
+ /**
+ * Building a UiSelector always returns a new UiSelector and never modifies the
+ * existing UiSelector being used.
+ */
+ private UiSelector buildSelector(int selectorId, Object selectorValue) {
+ UiSelector selector = new UiSelector(this);
+ if (selectorId == SELECTOR_CHILD || selectorId == SELECTOR_PARENT)
+ selector.getLastSubSelector().mSelectorAttributes.put(selectorId, selectorValue);
+ else
+ selector.mSelectorAttributes.put(selectorId, selectorValue);
+ return selector;
+ }
+
+ /**
+ * Selectors may have a hierarchy defined by specifying child nodes to be matched.
+ * It is not necessary that every selector have more than one level. A selector
+ * can also be a single level referencing only one node. In such cases the return
+ * it null.
+ *
+ * @return a child selector if one exists. Else null if this selector does not
+ * reference child node.
+ */
+ UiSelector getChildSelector() {
+ UiSelector selector = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD, null);
+ if (selector != null)
+ return new UiSelector(selector);
+ return null;
+ }
+
+ UiSelector getPatternSelector() {
+ UiSelector selector =
+ (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PATTERN, null);
+ if (selector != null)
+ return new UiSelector(selector);
+ return null;
+ }
+
+ UiSelector getContainerSelector() {
+ UiSelector selector =
+ (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CONTAINER, null);
+ if (selector != null)
+ return new UiSelector(selector);
+ return null;
+ }
+
+ UiSelector getParentSelector() {
+ UiSelector selector =
+ (UiSelector) mSelectorAttributes.get(UiSelector.SELECTOR_PARENT, null);
+ if (selector != null)
+ return new UiSelector(selector);
+ return null;
+ }
+
+ int getInstance() {
+ return getInt(UiSelector.SELECTOR_INSTANCE);
+ }
+
+ String getString(int criterion) {
+ return (String) mSelectorAttributes.get(criterion, null);
+ }
+
+ boolean getBoolean(int criterion) {
+ return (Boolean) mSelectorAttributes.get(criterion, false);
+ }
+
+ int getInt(int criterion) {
+ return (Integer) mSelectorAttributes.get(criterion, 0);
+ }
+
+ Pattern getPattern(int criterion) {
+ return (Pattern) mSelectorAttributes.get(criterion, null);
+ }
+
+ boolean isMatchFor(AccessibilityNodeInfo node, int index) {
+ int size = mSelectorAttributes.size();
+ for(int x = 0; x < size; x++) {
+ CharSequence s = null;
+ int criterion = mSelectorAttributes.keyAt(x);
+ switch(criterion) {
+ case UiSelector.SELECTOR_INDEX:
+ if (index != this.getInt(criterion))
+ return false;
+ break;
+ case UiSelector.SELECTOR_CHECKED:
+ if (node.isChecked() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CLASS:
+ s = node.getClassName();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CLASS_REGEX:
+ s = node.getClassName();
+ if (s == null || !getPattern(criterion).matcher(s).matches()) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CLICKABLE:
+ if (node.isClickable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CHECKABLE:
+ if (node.isCheckable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_LONG_CLICKABLE:
+ if (node.isLongClickable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CONTAINS_DESCRIPTION:
+ s = node.getContentDescription();
+ if (s == null || !s.toString().toLowerCase()
+ .contains(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_START_DESCRIPTION:
+ s = node.getContentDescription();
+ if (s == null || !s.toString().toLowerCase()
+ .startsWith(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_DESCRIPTION:
+ s = node.getContentDescription();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_DESCRIPTION_REGEX:
+ s = node.getContentDescription();
+ if (s == null || !getPattern(criterion).matcher(s).matches()) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_CONTAINS_TEXT:
+ s = node.getText();
+ if (s == null || !s.toString().toLowerCase()
+ .contains(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_START_TEXT:
+ s = node.getText();
+ if (s == null || !s.toString().toLowerCase()
+ .startsWith(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_TEXT:
+ s = node.getText();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_TEXT_REGEX:
+ s = node.getText();
+ if (s == null || !getPattern(criterion).matcher(s).matches()) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_ENABLED:
+ if (node.isEnabled() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_FOCUSABLE:
+ if (node.isFocusable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_FOCUSED:
+ if (node.isFocused() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_ID:
+ break; //TODO: do we need this for AccessibilityNodeInfo.id?
+ case UiSelector.SELECTOR_PACKAGE_NAME:
+ s = node.getPackageName();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_PACKAGE_NAME_REGEX:
+ s = node.getPackageName();
+ if (s == null || !getPattern(criterion).matcher(s).matches()) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_SCROLLABLE:
+ if (node.isScrollable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_SELECTED:
+ if (node.isSelected() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_RESOURCE_ID:
+ s = node.getViewIdResourceName();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case UiSelector.SELECTOR_RESOURCE_ID_REGEX:
+ s = node.getViewIdResourceName();
+ if (s == null || !getPattern(criterion).matcher(s).matches()) {
+ return false;
+ }
+ break;
+ }
+ }
+ return matchOrUpdateInstance();
+ }
+
+ private boolean matchOrUpdateInstance() {
+ int currentSelectorCounter = 0;
+ int currentSelectorInstance = 0;
+
+ // matched attributes - now check for matching instance number
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) {
+ currentSelectorInstance =
+ (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
+ }
+
+ // instance is required. Add count if not already counting
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) {
+ currentSelectorCounter = (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
+ }
+
+ // Verify
+ if (currentSelectorInstance == currentSelectorCounter) {
+ return true;
+ }
+ // Update count
+ if (currentSelectorInstance > currentSelectorCounter) {
+ mSelectorAttributes.put(UiSelector.SELECTOR_COUNT, ++currentSelectorCounter);
+ }
+ return false;
+ }
+
+ /**
+ * Leaf selector indicates no more child or parent selectors
+ * are declared in the this selector.
+ * @return true if is leaf.
+ */
+ boolean isLeaf() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0 &&
+ mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
+ return true;
+ }
+ return false;
+ }
+
+ boolean hasChildSelector() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasPatternSelector() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasContainerSelector() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasParentSelector() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the deepest selector in the chain of possible sub selectors.
+ * A chain of selector is created when either of {@link UiSelector#childSelector(UiSelector)}
+ * or {@link UiSelector#fromParent(UiSelector)} are used once or more in the construction of
+ * a selector.
+ * @return last UiSelector in chain
+ */
+ private UiSelector getLastSubSelector() {
+ if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0) {
+ UiSelector child = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_CHILD);
+ if (child.getLastSubSelector() == null) {
+ return child;
+ }
+ return child.getLastSubSelector();
+ } else if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0) {
+ UiSelector parent = (UiSelector)mSelectorAttributes.get(UiSelector.SELECTOR_PARENT);
+ if (parent.getLastSubSelector() == null) {
+ return parent;
+ }
+ return parent.getLastSubSelector();
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return dumpToString(true);
+ }
+
+ String dumpToString(boolean all) {
+ StringBuilder builder = new StringBuilder();
+ builder.append(UiSelector.class.getSimpleName() + "[");
+ final int criterionCount = mSelectorAttributes.size();
+ for (int i = 0; i < criterionCount; i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ final int criterion = mSelectorAttributes.keyAt(i);
+ switch (criterion) {
+ case SELECTOR_TEXT:
+ builder.append("TEXT=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_TEXT_REGEX:
+ builder.append("TEXT_REGEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_START_TEXT:
+ builder.append("START_TEXT=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CONTAINS_TEXT:
+ builder.append("CONTAINS_TEXT=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CLASS:
+ builder.append("CLASS=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CLASS_REGEX:
+ builder.append("CLASS_REGEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_DESCRIPTION:
+ builder.append("DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_DESCRIPTION_REGEX:
+ builder.append("DESCRIPTION_REGEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_START_DESCRIPTION:
+ builder.append("START_DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CONTAINS_DESCRIPTION:
+ builder.append("CONTAINS_DESCRIPTION=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_INDEX:
+ builder.append("INDEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_INSTANCE:
+ builder.append("INSTANCE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_ENABLED:
+ builder.append("ENABLED=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_FOCUSED:
+ builder.append("FOCUSED=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_FOCUSABLE:
+ builder.append("FOCUSABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_SCROLLABLE:
+ builder.append("SCROLLABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CLICKABLE:
+ builder.append("CLICKABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CHECKABLE:
+ builder.append("CHECKABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_LONG_CLICKABLE:
+ builder.append("LONG_CLICKABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CHECKED:
+ builder.append("CHECKED=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_SELECTED:
+ builder.append("SELECTED=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_ID:
+ builder.append("ID=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_CHILD:
+ if (all)
+ builder.append("CHILD=").append(mSelectorAttributes.valueAt(i));
+ else
+ builder.append("CHILD[..]");
+ break;
+ case SELECTOR_PATTERN:
+ if (all)
+ builder.append("PATTERN=").append(mSelectorAttributes.valueAt(i));
+ else
+ builder.append("PATTERN[..]");
+ break;
+ case SELECTOR_CONTAINER:
+ if (all)
+ builder.append("CONTAINER=").append(mSelectorAttributes.valueAt(i));
+ else
+ builder.append("CONTAINER[..]");
+ break;
+ case SELECTOR_PARENT:
+ if (all)
+ builder.append("PARENT=").append(mSelectorAttributes.valueAt(i));
+ else
+ builder.append("PARENT[..]");
+ break;
+ case SELECTOR_COUNT:
+ builder.append("COUNT=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_PACKAGE_NAME:
+ builder.append("PACKAGE NAME=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_PACKAGE_NAME_REGEX:
+ builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_RESOURCE_ID:
+ builder.append("RESOURCE_ID=").append(mSelectorAttributes.valueAt(i));
+ break;
+ case SELECTOR_RESOURCE_ID_REGEX:
+ builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i));
+ break;
+ default:
+ builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i));
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+}
diff --git a/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java
new file mode 100644
index 0000000..5403e30
--- /dev/null
+++ b/cmds/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * See {@link UiDevice#registerWatcher(String, UiWatcher)} on how to register a
+ * a condition watcher to be called by the automation library. The automation library will
+ * invoke checkForCondition() only when a regular API call is in retry mode because it is unable
+ * to locate its selector yet. Only during this time, the watchers are invoked to check if there is
+ * something else unexpected on the screen.
+ * @since API Level 16
+ */
+public interface UiWatcher {
+
+ /**
+ * Custom handler that is automatically called when the testing framework is unable to
+ * find a match using the {@link UiSelector}
+ *
+ * When the framework is in the process of matching a {@link UiSelector} and it
+ * is unable to match any widget based on the specified criteria in the selector,
+ * the framework will perform retries for a predetermined time, waiting for the display
+ * to update and show the desired widget. While the framework is in this state, it will call
+ * registered watchers' checkForCondition(). This gives the registered watchers a chance
+ * to take a look at the display and see if there is a recognized condition that can be
+ * handled and in doing so allowing the current test to continue.
+ *
+ * An example usage would be to look for dialogs popped due to other background
+ * processes requesting user attention and have nothing to do with the application
+ * currently under test.
+ *
+ * @return true to indicate a matched condition or false for nothing was matched
+ * @since API Level 16
+ */
+ public boolean checkForCondition();
+}