summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuang Zhu <guangzhu@google.com>2012-06-13 18:19:49 -0700
committerGuang Zhu <guangzhu@google.com>2012-06-15 11:58:45 -0700
commite54d649fb83a0a44516e5c25a9ac1992c8950e59 (patch)
treeb7fed360992f45b775e57597f1dbb1d0a838577c
parent83ad9f36a8f34dfb2b7280667ba1f2f02282ac72 (diff)
downloadtesting-e54d649fb83a0a44516e5c25a9ac1992c8950e59.tar.gz
uiautomator branding and source move
This moves the source of core pieces of uiautomator framework: * module uiautomator.core: core classeses, including test runner * module uiautomator: command line runner Modules are building into system image, but they are marked optional for now. No material Java code changes are made, only package names, imports and makefiles are modified as needed. Change-Id: I09816368c02203fed2eeabd4f73b93111d1d4b29
-rw-r--r--Android.mk17
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--uiautomator/Android.mk17
-rw-r--r--uiautomator/MODULE_LICENSE_APACHE20
-rw-r--r--uiautomator/cmds/Android.mk17
-rw-r--r--uiautomator/cmds/uiautomator/Android.mk34
-rw-r--r--uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/UiAutomator.java157
-rwxr-xr-xuiautomator/cmds/uiautomator/uiautomator62
-rw-r--r--uiautomator/library/Android.mk24
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java199
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/By.java570
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/InteractionController.java499
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/QueryController.java517
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java201
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiCollection.java126
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiDevice.java646
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiObject.java751
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java38
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java475
-rw-r--r--uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java27
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java35
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java141
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java142
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java41
-rw-r--r--uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java332
25 files changed, 5068 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..c141484
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/uiautomator/Android.mk b/uiautomator/Android.mk
new file mode 100644
index 0000000..c141484
--- /dev/null
+++ b/uiautomator/Android.mk
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
diff --git a/uiautomator/MODULE_LICENSE_APACHE2 b/uiautomator/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/uiautomator/MODULE_LICENSE_APACHE2
diff --git a/uiautomator/cmds/Android.mk b/uiautomator/cmds/Android.mk
new file mode 100644
index 0000000..c141484
--- /dev/null
+++ b/uiautomator/cmds/Android.mk
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
diff --git a/uiautomator/cmds/uiautomator/Android.mk b/uiautomator/cmds/uiautomator/Android.mk
new file mode 100644
index 0000000..d3b9c85
--- /dev/null
+++ b/uiautomator/cmds/uiautomator/Android.mk
@@ -0,0 +1,34 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := uiautomator.core
+LOCAL_MODULE := uiautomator.cmd
+
+include $(BUILD_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := uiautomator_shell_exe
+LOCAL_MODULE_STEM := uiautomator
+LOCAL_SRC_FILES := uiautomator
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_PREBUILT)
diff --git a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/UiAutomator.java b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/UiAutomator.java
new file mode 100644
index 0000000..dba7b09
--- /dev/null
+++ b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/UiAutomator.java
@@ -0,0 +1,157 @@
+/*
+ * 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.commands.uiautomator;
+
+import android.os.Bundle;
+import android.os.Process;
+
+import com.android.uiautomator.testrunner.UiAutomatorTestRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UiAutomator {
+
+ private static final String CLASS_PARAM = "class";
+ private static final String DEBUG_PARAM = "debug";
+ private static final String RUNNER_PARAM = "runner";
+ private static final String CLASS_SEPARATOR = ",";
+ private static final int ARG_OK = 0;
+ private static final int ARG_FAIL_INCOMPLETE_E = -1;
+ private static final int ARG_FAIL_INCOMPLETE_C = -2;
+ private static final int ARG_FAIL_NO_CLASS = -3;
+ private static final int ARG_FAIL_RUNNER = -4;
+ private static final int ARG_FAIL_UNSUPPORTED = -99;
+
+ private Bundle mParams = new Bundle();
+ private List<String> mTestClasses = new ArrayList<String>();
+ private boolean mDebug;
+ private String mRunner;
+
+ public static void main(String[] args) {
+ // set the name as it appears in ps/top etc
+ Process.setArgV0("uiautomator");
+ new UiAutomator().run(args);
+ }
+
+ private void run(String[] args) {
+ int ret = parseArgs(args);
+ switch (ret) {
+ case ARG_FAIL_INCOMPLETE_C:
+ System.err.println("Incomplete '-c' parameter.");
+ System.exit(ARG_FAIL_INCOMPLETE_C);
+ break;
+ case ARG_FAIL_INCOMPLETE_E:
+ System.err.println("Incomplete '-e' parameter.");
+ System.exit(ARG_FAIL_INCOMPLETE_E);
+ break;
+ case ARG_FAIL_UNSUPPORTED:
+ System.err.println("Unsupported standaline parameter.");
+ System.exit(ARG_FAIL_UNSUPPORTED);
+ break;
+ default:
+ break;
+ }
+ if (mTestClasses.isEmpty()) {
+ System.err.println("Please specify at least one test class to run.");
+ System.exit(ARG_FAIL_NO_CLASS);
+ }
+ getRunner().run(mTestClasses, mParams, mDebug);
+ }
+
+ private int parseArgs(String[] args) {
+ // we are parsing for these parameters:
+ // -e <key> <value>
+ // key-value pairs
+ // special ones are:
+ // key is "class", parameter is passed onto JUnit as class name to run
+ // key is "debug", parameter will determine whether to wait for debugger
+ // to attach
+ // -c <class name>
+ // equivalent to -e class <class name>, i.e. passed onto JUnit
+ for (int i = 0; i < args.length; i++) {
+ if (args[i].equals("-e")) {
+ if (i + 2 < args.length) {
+ String key = args[++i];
+ String value = args[++i];
+ if (CLASS_PARAM.equals(key)) {
+ addTestClasses(value);
+ } else if (DEBUG_PARAM.equals(key)) {
+ mDebug = "true".equals(value) || "1".equals(value);
+ } else if (RUNNER_PARAM.equals(key)) {
+ mRunner = value;
+ } else {
+ mParams.putString(key, value);
+ }
+ } else {
+ return ARG_FAIL_INCOMPLETE_E;
+ }
+ } else if (args[i].equals("-c")) {
+ if (i + 1 < args.length) {
+ addTestClasses(args[++i]);
+ } else {
+ return ARG_FAIL_INCOMPLETE_C;
+ }
+ } else {
+ return ARG_FAIL_UNSUPPORTED;
+ }
+ }
+ return ARG_OK;
+ }
+
+ protected UiAutomatorTestRunner getRunner() {
+ if (mRunner == null) {
+ return new UiAutomatorTestRunner();
+ }
+ // use reflection to get the runner
+ Object o = null;
+ try {
+ Class<?> clazz = Class.forName(mRunner);
+ o = clazz.newInstance();
+ } catch (ClassNotFoundException cnfe) {
+ System.err.println("Cannot find runner: " + mRunner);
+ System.exit(ARG_FAIL_RUNNER);
+ } catch (InstantiationException ie) {
+ System.err.println("Cannot instantiate runner: " + mRunner);
+ System.exit(ARG_FAIL_RUNNER);
+ } catch (IllegalAccessException iae) {
+ System.err.println("Constructor of runner " + mRunner + " is not accessibile");
+ System.exit(ARG_FAIL_RUNNER);
+ }
+ try {
+ UiAutomatorTestRunner runner = (UiAutomatorTestRunner)o;
+ return runner;
+ } catch (ClassCastException cce) {
+ System.err.println("Specified runner is not subclass of "
+ + UiAutomatorTestRunner.class.getSimpleName());
+ System.exit(ARG_FAIL_RUNNER);
+ }
+ // won't reach here
+ return null;
+ }
+
+ /**
+ * Add test classes from a potentially comma separated list
+ * @param classes
+ */
+ private void addTestClasses(String classes) {
+ String[] classArray = classes.split(CLASS_SEPARATOR);
+ for (String clazz : classArray) {
+ mTestClasses.add(clazz);
+ }
+ }
+} \ No newline at end of file
diff --git a/uiautomator/cmds/uiautomator/uiautomator b/uiautomator/cmds/uiautomator/uiautomator
new file mode 100755
index 0000000..920245c
--- /dev/null
+++ b/uiautomator/cmds/uiautomator/uiautomator
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+#
+
+# Script to start "uiautomator" on the device
+export run_base=/data/local/tmp
+export base=/system
+
+# if not running as root, trick dalvik into using an alternative dex cache
+if [ $USER_ID -ne 0 ]; then
+ tmp_cache=${run_base}/dalvik-cache
+
+ if [ ! -d $tmp_cache ]; then
+ mkdir -p $tmp_cache
+ fi
+
+ export ANDROID_DATA=${run_base}
+fi
+
+if [ "$1" = "--nohup" ]; then
+ trap "" HUP
+ shift
+fi
+
+if [ -z "$1" ]; then
+ echo "Need path to tests jar file"
+ exit 1
+fi
+
+# check if the supplied jar path is an absolute path
+if [ "${1:0:1}" = "/" ]; then
+ jarpath=$1
+else
+ jarpath=${run_base}/$1
+fi
+
+if [ ! -f ${jarpth} ]; then
+ echo "$1 must be an existing file in ${run_base}"
+ exit 1
+fi
+
+CLASSPATH=/system/framework/android.test.runner.jar:${base}/framework/uiautomator.cmd.jar
+CLASSPATH=${CLASSPATH}:${jarpath}
+
+# strip the first parameter
+shift
+
+export CLASSPATH
+exec app_process ${base}/bin com.android.commands.uiautomator.UiAutomator "$@"
diff --git a/uiautomator/library/Android.mk b/uiautomator/library/Android.mk
new file mode 100644
index 0000000..031b753
--- /dev/null
+++ b/uiautomator/library/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE := uiautomator.core
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
new file mode 100644
index 0000000..ab68120
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
@@ -0,0 +1,199 @@
+/*
+ * 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.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;
+
+public class AccessibilityNodeInfoDumper {
+
+ private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
+ private static final String[] EXCLUDED_CLASSES = new String[]
+ {"LinearLayout", "RelativeLayout", "ListView"};
+
+ /**
+ * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
+ * and generates an xml dump into the /data/local/window_dump.xml
+ * @param info
+ */
+ public static void dumpWindowToFile(AccessibilityNodeInfo info) {
+ 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(info, new File(
+ new File(Environment.getDataDirectory(), "local"), "window_dump.xml"));
+ }
+
+ /**
+ * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
+ * and generates an xml dump to the location specified by <code>dumpFile</code>
+ * @param info
+ */
+ public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile) {
+ 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");
+ dumpNodeRec(root, serializer, 0);
+ 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)
+ throws IOException {
+ if(!excludedClass(node) && !accessibilityCheck(node)) {
+ serializer.comment("NAF: The following control may not be accessibility friendly");
+ serializer.startTag("", "node");
+ serializer.attribute("", "NAF", Boolean.toString(true));
+ } else {
+ serializer.startTag("", "node");
+ }
+ serializer.attribute("", "index", Integer.toString(index));
+ serializer.attribute("", "text", safeCharSeqToString(node.getText()));
+ 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()));
+ Rect bounds = new Rect();
+ node.getBoundsInScreen(bounds);
+ serializer.attribute("", "bounds", bounds.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);
+ 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 n
+ * @return
+ */
+ private static boolean excludedClass(AccessibilityNodeInfo n) {
+ String className = safeCharSeqToString(n.getClassName());
+ for(String excludedClassName : 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.
+ * @param n
+ * @return
+ */
+ private static boolean accessibilityCheck(AccessibilityNodeInfo n) {
+ return !(n.isClickable() && n.isEnabled() &&
+ safeCharSeqToString(n.getContentDescription()).isEmpty() &&
+ safeCharSeqToString(n.getText()).isEmpty());
+ }
+
+ 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/uiautomator/library/src/com/android/uiautomator/core/By.java b/uiautomator/library/src/com/android/uiautomator/core/By.java
new file mode 100644
index 0000000..51a8936
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/By.java
@@ -0,0 +1,570 @@
+/*
+ * 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;
+
+/**
+ * This class provides the mechanism for tests to describe the UI elements they
+ * intend to target. A UI element has many properties associated with it such as
+ * text values, a content-description field, class name and multiple state
+ * information like isSelected, isEnabled or isChecked. Additionally a UI element
+ * may also be associated with a specific layout hierarchy that the test wishes
+ * to use to unambiguously target one UI element separated from other similar ones.
+ * <p/> This class will allow tests to create the instructions necessary for the
+ * the {@link UiObject} and {@link UiScrollable} classes to uses to target the UI
+ * element for the purposes of verifications, interactions or enumerations.
+ */
+public class By {
+ 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;
+
+ private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>();
+ public static final String LOG_TAG = "ByClass";
+
+ private By() {
+ }
+
+ private By(By by) {
+ mSelectorAttributes = by.cloneSelectors().mSelectorAttributes;
+ }
+
+ protected By cloneSelectors() {
+ By ret = By.selector();
+
+ // shallow copy the attributes
+ ret.mSelectorAttributes = mSelectorAttributes.clone();
+
+ // deep copy attributes that contain sub selectors
+ By by = getChildSelector();
+ if(by != null) {
+ mSelectorAttributes.put(By.SELECTOR_CHILD, by.cloneSelectors());
+ }
+
+ by = getContainerSelector();
+ if(by != null) {
+ mSelectorAttributes.put(By.SELECTOR_CONTAINER, by.cloneSelectors());
+ }
+
+ by = getPatternSelector();
+ if(by != null) {
+ mSelectorAttributes.put(By.SELECTOR_PATTERN, by.cloneSelectors());
+ }
+
+ by = getParentSelector();
+ if(by != null) {
+ mSelectorAttributes.put(By.SELECTOR_PARENT, by.cloneSelectors());
+ }
+
+ return ret;
+ }
+
+ public static By selector() {
+ return new By();
+ }
+
+ static By selector(By selector) {
+ return new By(selector);
+ }
+
+ static By patternBuilder(By selector) {
+ if(!selector.hasPatternSelector()) {
+ return By.selector().patternSelector(selector);
+ }
+ return selector;
+ }
+
+ static By patternBuilder(By container, By pattern) {
+ return By.selector(By.selector().containerSelector(container).patternSelector(pattern));
+ }
+
+
+ public By text(String text) {
+ return buildSelector(SELECTOR_TEXT, text);
+ }
+
+ public By textStartsWith(String text) {
+ return buildSelector(SELECTOR_START_TEXT, text);
+ }
+
+ public By textContains(String text) {
+ return buildSelector(SELECTOR_CONTAINS_TEXT, text);
+ }
+
+ public By className(String className) {
+ return buildSelector(SELECTOR_CLASS, className);
+ }
+
+ public By description(String desc) {
+ return buildSelector(SELECTOR_DESCRIPTION, desc);
+ }
+
+ public By descriptionStartsWith(String desc) {
+ return buildSelector(SELECTOR_START_DESCRIPTION, desc);
+ }
+
+ public By descriptionContains(String desc) {
+ return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
+ }
+
+ public By index(final int index) {
+ return buildSelector(SELECTOR_INDEX, index);
+ }
+
+ public By instance(final int instance) {
+ return buildSelector(SELECTOR_INSTANCE, instance);
+ }
+
+ public By enabled(boolean val) {
+ return buildSelector(SELECTOR_ENABLED, val);
+ }
+
+ public By focused(boolean val) {
+ return buildSelector(SELECTOR_FOCUSED, val);
+ }
+
+ public By focusable(boolean val) {
+ return buildSelector(SELECTOR_FOCUSABLE, val);
+ }
+
+ public By scrollable(boolean val) {
+ return buildSelector(SELECTOR_SCROLLABLE, val);
+ }
+
+ public By selected(boolean val) {
+ return buildSelector(SELECTOR_SELECTED, val);
+ }
+
+ public By checked(boolean val) {
+ return buildSelector(SELECTOR_CHECKED, val);
+ }
+
+ public By clickable(boolean val) {
+ return buildSelector(SELECTOR_CLICKABLE, val);
+ }
+
+ public By childSelector(By by) {
+ return buildSelector(SELECTOR_CHILD, by);
+ }
+
+ private By patternSelector(By by) {
+ return buildSelector(SELECTOR_PATTERN, by);
+ }
+
+ private By containerSelector(By by) {
+ return buildSelector(SELECTOR_CONTAINER, by);
+ }
+
+ public By fromParent(By by) {
+ return buildSelector(SELECTOR_PARENT, by);
+ }
+
+ public By packageName(String name) {
+ return buildSelector(SELECTOR_PACKAGE_NAME, name);
+ }
+
+ /**
+ * Building a By selector always returns a new By selector and never modifies the
+ * existing By selector being used. For example a test library have predefined
+ * By selectors SA, SB, SC etc. The test may decide that it needs a By selector SX in the
+ * context of SB. It can use SX = SB.critereon(x) to generate SX without modifying the
+ * state of SB which is expected to be something else. For this we will return a new
+ * By selector every time.
+ */
+ private By buildSelector(int selectorId, Object selectorValue) {
+ By by = By.selector(this);
+ if(selectorId == SELECTOR_CHILD || selectorId == SELECTOR_PARENT)
+ by.getLastSubSelector().mSelectorAttributes.put(selectorId, selectorValue);
+ else
+ by.mSelectorAttributes.put(selectorId, selectorValue);
+ return by;
+ }
+
+ /**
+ * 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.
+ */
+
+ By getChildSelector() {
+ By by = (By)mSelectorAttributes.get(By.SELECTOR_CHILD, null);
+ if(by != null)
+ return By.selector(by);
+ return null;
+ }
+
+ By getPatternSelector() {
+ By by = (By)mSelectorAttributes.get(By.SELECTOR_PATTERN, null);
+ if(by != null)
+ return By.selector(by);
+ return null;
+ }
+
+ By getContainerSelector() {
+ By by = (By)mSelectorAttributes.get(By.SELECTOR_CONTAINER, null);
+ if(by != null)
+ return By.selector(by);
+ return null;
+ }
+
+ By getParentSelector() {
+ By by = (By) mSelectorAttributes.get(By.SELECTOR_PARENT, null);
+ if(by != null)
+ return By.selector(by);
+ return null;
+ }
+
+ int getInstance() {
+ return getInt(By.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);
+ }
+
+ 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 By.SELECTOR_INDEX:
+ if(index != this.getInt(criterion))
+ return false;
+ break;
+ case By.SELECTOR_CHECKED:
+ if (node.isChecked() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_CLASS:
+ s = node.getClassName();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_CLICKABLE:
+ if (node.isClickable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_CONTAINS_DESCRIPTION:
+ s = node.getContentDescription();
+ if(s == null || !s.toString().toLowerCase()
+ .contains(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_START_DESCRIPTION:
+ s = node.getContentDescription();
+ if(s == null || !s.toString().toLowerCase()
+ .startsWith(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_DESCRIPTION:
+ s = node.getContentDescription();
+ if(s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_CONTAINS_TEXT:
+ s = node.getText();
+ if(s == null || !s.toString().toLowerCase()
+ .contains(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_START_TEXT:
+ s = node.getText();
+ if(s == null || !s.toString().toLowerCase()
+ .startsWith(getString(criterion).toLowerCase())) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_TEXT:
+ s = node.getText();
+ if(s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_ENABLED:
+ if(node.isEnabled() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_FOCUSABLE:
+ if(node.isFocusable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_FOCUSED:
+ if(node.isFocused() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_ID:
+ break; //TODO: do we need this for AccessibilityNodeInfo.id?
+ case By.SELECTOR_PACKAGE_NAME:
+ s = node.getPackageName();
+ if(s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_SCROLLABLE:
+ if(node.isScrollable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
+ case By.SELECTOR_SELECTED:
+ if(node.isSelected() != getBoolean(criterion)) {
+ 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(By.SELECTOR_INSTANCE) > 0) {
+ currentSelectorInstance = (Integer)mSelectorAttributes.get(By.SELECTOR_INSTANCE);
+ }
+
+ // instance is required. Add count if not already counting
+ if(mSelectorAttributes.indexOfKey(By.SELECTOR_COUNT) > 0) {
+ currentSelectorCounter = (Integer)mSelectorAttributes.get(By.SELECTOR_COUNT);
+ }
+
+ // Verify
+ if (currentSelectorInstance == currentSelectorCounter) {
+ return true;
+ }
+ // Update count
+ if (currentSelectorInstance > currentSelectorCounter) {
+ mSelectorAttributes.put(By.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(By.SELECTOR_CHILD) < 0 &&
+ mSelectorAttributes.indexOfKey(By.SELECTOR_PARENT) < 0) {
+ return true;
+ }
+ return false;
+ }
+
+ boolean hasChildSelector() {
+ if(mSelectorAttributes.indexOfKey(By.SELECTOR_CHILD) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasPatternSelector() {
+ if(mSelectorAttributes.indexOfKey(By.SELECTOR_PATTERN) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasContainerSelector() {
+ if(mSelectorAttributes.indexOfKey(By.SELECTOR_CONTAINER) < 0) {
+ return false;
+ }
+ return true;
+ }
+
+ boolean hasParentSelector() {
+ if(mSelectorAttributes.indexOfKey(By.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 By#childSelector(By)}
+ * or {@link By#fromParent(By)} are used once or more in the construction of
+ * a selector.
+ * @return last By selector in chain
+ */
+ private By getLastSubSelector() {
+ if(mSelectorAttributes.indexOfKey(By.SELECTOR_CHILD) >= 0) {
+ By child = (By)mSelectorAttributes.get(By.SELECTOR_CHILD);
+ if(child.getLastSubSelector() == null) {
+ return child;
+ }
+ return child.getLastSubSelector();
+ } else if(mSelectorAttributes.indexOfKey(By.SELECTOR_PARENT) >= 0) {
+ By parent = (By)mSelectorAttributes.get(By.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("By[");
+ 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_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_DESCRIPTION:
+ builder.append("DESCRIPTION=").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_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;
+ default:
+ builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i));
+ }
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java
new file mode 100644
index 0000000..2d9301f
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java
@@ -0,0 +1,499 @@
+/*
+ * 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.ActivityManagerNative;
+import android.app.IActivityManager;
+import android.app.IActivityManager.ContentProviderHolder;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.database.Cursor;
+import android.graphics.Point;
+import android.hardware.input.InputManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IPowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.IWindowManager;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.util.Predicate;
+
+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 = false;
+
+ private static final long DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS = 500;
+
+ private final KeyCharacterMap mKeyCharacterMap =
+ KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+
+ private final UiAutomatorBridge mUiAutomatorBridge;
+
+ private final IWindowManager mWindowManager;
+
+ private final long mLongPressTimeout;
+
+ private long mDownTime;
+
+ public InteractionController(UiAutomatorBridge bridge) {
+ mUiAutomatorBridge = bridge;
+
+ // Obtain the window manager.
+ mWindowManager = IWindowManager.Stub.asInterface(
+ ServiceManager.getService(Context.WINDOW_SERVICE));
+ if (mWindowManager == null) {
+ throw new RuntimeException("Unable to connect to WindowManager, "
+ + "is the system running?");
+ }
+
+ // the value returned is on the border of going undetected as used
+ // by this framework during long presses. Adding few extra 100ms
+ // of long press time helps ensure long enough time for a valid
+ // longClick detection.
+ mLongPressTimeout = getSystemLongPressTime() * 2 + 100;
+ }
+
+ /**
+ * Get the system long press time
+ * @return milliseconds
+ */
+ private long getSystemLongPressTime() {
+ // Read the long press timeout setting.
+ long longPressTimeout = 0;
+ try {
+ IContentProvider provider = null;
+ Cursor cursor = null;
+ IActivityManager activityManager = ActivityManagerNative.getDefault();
+ String providerName = Settings.Secure.CONTENT_URI.getAuthority();
+ IBinder token = new Binder();
+ try {
+ ContentProviderHolder holder = activityManager.getContentProviderExternal(
+ providerName, token);
+ if (holder == null) {
+ throw new IllegalStateException("Could not find provider: " + providerName);
+ }
+ provider = holder.provider;
+ cursor = provider.query(Settings.Secure.CONTENT_URI,
+ new String[] {Settings.Secure.VALUE}, "name=?",
+ new String[] {Settings.Secure.LONG_PRESS_TIMEOUT}, null, null);
+ if (cursor.moveToFirst()) {
+ longPressTimeout = cursor.getInt(0);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ if (provider != null) {
+ activityManager.removeContentProviderExternal(providerName, token);
+ }
+ }
+ } catch (RemoteException e) {
+ String message = "Error reading long press timeout setting.";
+ Log.e(LOG_TAG, message, e);
+ throw new RuntimeException(message, e);
+ }
+ return longPressTimeout;
+ }
+
+ public boolean tap(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "tap (" + x + ", " + y + ")");
+ }
+
+ mUiAutomatorBridge.setOperationTime();
+ if (touchDown(x, y)) {
+ if(touchUp(x, y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean tapAndWaitForNewWindow(final int x, final int y, long timeout) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "tap (" + x + ", " + y + ")");
+ }
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ touchDown(x, y);
+ touchUp(x, y);
+ }
+ };
+ Predicate<AccessibilityEvent> predicate = new Predicate<AccessibilityEvent>() {
+ @Override
+ public boolean apply(AccessibilityEvent t) {
+ return t.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
+ }
+ };
+ try {
+ mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(
+ command, predicate, timeout);
+ } catch (TimeoutException e) {
+ return false;
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
+ return false;
+ }
+ return true;
+ }
+
+ public boolean longTap(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "longTap (" + x + ", " + y + ")");
+ }
+
+ mUiAutomatorBridge.setOperationTime();
+ if (touchDown(x, y)) {
+ SystemClock.sleep(mLongPressTimeout);
+ 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, 0);
+ 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, 0);
+ 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, 0);
+ 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 duration
+ * @return true if the swipe and scrolling have been successfully completed.
+ */
+ 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 +")");
+ try {
+ mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(
+ new Runnable() {
+ @Override
+ public void run() {
+ swipe(downX, downY, upX, upY, steps);
+ }
+ },
+ new Predicate<AccessibilityEvent>() {
+ @Override
+ public boolean apply(AccessibilityEvent event) {
+ return (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ }
+ }, DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Error in scrollSwipe: " + e.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle swipes in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param duration
+ * @return
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
+ 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);
+ 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(5);
+ }
+ 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(5);
+ }
+ }
+ }
+ 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 + ")");
+ }
+
+ mUiAutomatorBridge.setOperationTime();
+ KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
+ if (events != null) {
+ 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;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean sendKey(int keyCode, int metaState) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
+ }
+
+ mUiAutomatorBridge.setOperationTime();
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = KeyEvent.obtain(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD, null);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = KeyEvent.obtain(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD, null);
+ if(injectEventSync(upEvent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if the device is in its natural orientation. This is determined by
+ * checking whether the orientation is at 0 or 180 degrees.
+ * @return true if it is in natural orientation
+ * @throws RemoteException
+ */
+ public boolean isNaturalRotation() throws RemoteException {
+ return mWindowManager.getRotation() == Surface.ROTATION_0
+ || mWindowManager.getRotation() == Surface.ROTATION_180;
+ }
+
+ /**
+ * 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() throws RemoteException {
+ mWindowManager.freezeRotation(Surface.ROTATION_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() throws RemoteException {
+ mWindowManager.freezeRotation(Surface.ROTATION_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() throws RemoteException {
+ mWindowManager.freezeRotation(Surface.ROTATION_0);
+ }
+
+ /**
+ * Disables the sensors and freezes the device rotation at its
+ * current rotation state.
+ * @throws RemoteException
+ */
+ public void freezeRotation() throws RemoteException {
+ mWindowManager.freezeRotation(-1);
+ }
+
+ /**
+ * 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() throws RemoteException {
+ mWindowManager.thawRotation();
+ }
+
+ /**
+ * 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 {
+ IPowerManager pm =
+ IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ return pm.isScreenOn();
+ }
+
+ private static boolean injectEventSync(InputEvent event) {
+ return InputManager.getInstance().injectInputEvent(event,
+ InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/QueryController.java b/uiautomator/library/src/com/android/uiautomator/core/QueryController.java
new file mode 100644
index 0000000..3e2d81f
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/QueryController.java
@@ -0,0 +1,517 @@
+/*
+ * 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.SystemClock;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.uiautomator.core.UiAutomatorBridge.AccessibilityEventListener;
+
+/**
+ * The QuertController main purpose is to translate a {@link By} selectors to
+ * {@link AccessibilityNodeInfo}. This is all this controller does. It is typically
+ * created in conjunction with a {@link InteractionController} by {@link UiAutomationContext}
+ * which owns both. {@link UiAutomationContext} is used by {@link UiBase} classes.
+ */
+class QueryController {
+
+ private static final String LOG_TAG = QueryController.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ private final UiAutomatorBridge mUiAutomatorBridge;
+
+ private final Object mLock = new Object();
+
+ private String mLastActivityName = null;
+ private String mLastPackageName = 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.addAccessibilityEventListener(new AccessibilityEventListener() {
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ synchronized (mLock) {
+ mLastPackageName = event.getPackageName().toString();
+ // 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();
+ }
+
+ switch(event.getEventType()) {
+ 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.i(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.
+ * @return
+ */
+ 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(By selector) {
+ findAccessibilityNodeInfo(selector, true /*counting*/);
+ return mPatternCounter;
+ }
+
+ /**
+ * Main search method for translating By selectors to AccessibilityInfoNodes
+ * @param selector
+ * @return
+ */
+ public AccessibilityNodeInfo findAccessibilityNodeInfo(By selector) {
+ return findAccessibilityNodeInfo(selector, false);
+ }
+
+ protected AccessibilityNodeInfo findAccessibilityNodeInfo(By selector, boolean isCounting) {
+ mUiAutomatorBridge.waitForIdle();
+ initializeNewSearch();
+
+ if(DEBUG)
+ Log.i(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
+ By bySelector = By.selector(selector);
+ return translateCompoundSelector(bySelector, 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.getRootAccessibilityNodeInfoInActiveWindow();
+ 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 bySelector
+ * @param fromNode
+ * @param isCounting
+ * @return
+ */
+ private AccessibilityNodeInfo translateCompoundSelector(By bySelector,
+ 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(bySelector.hasContainerSelector())
+ // nested pattern selectors
+ if(bySelector.getContainerSelector().hasContainerSelector()) {
+ fromNode = translateCompoundSelector(
+ bySelector.getContainerSelector(), fromNode, false);
+ initializeNewSearch();
+ } else
+ fromNode = translateReqularSelector(bySelector.getContainerSelector(), fromNode);
+ else
+ fromNode = translateReqularSelector(bySelector, fromNode);
+
+ if(fromNode == null) {
+ if(DEBUG)
+ Log.i(LOG_TAG, "Container selector not found: " + bySelector.dumpToString(false));
+ return null;
+ }
+
+ if(bySelector.hasPatternSelector()) {
+ fromNode = translatePatternSelector(bySelector.getPatternSelector(),
+ fromNode, isCounting);
+
+ if (isCounting) {
+ Log.i(LOG_TAG, String.format(
+ "Counted %d instances of: %s", mPatternCounter, bySelector));
+ return null;
+ } else {
+ if(fromNode == null) {
+ if(DEBUG)
+ Log.i(LOG_TAG, "Pattern selector not found: " +
+ bySelector.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(bySelector.hasContainerSelector() || bySelector.hasPatternSelector()) {
+ if(bySelector.hasChildSelector() || bySelector.hasParentSelector())
+ fromNode = translateReqularSelector(bySelector, fromNode);
+ }
+
+ if(fromNode == null) {
+ if(DEBUG)
+ Log.i(LOG_TAG, "Object Not Found for selector " + bySelector);
+ return null;
+ }
+ Log.i(LOG_TAG, String.format("Matched selector: %s <<==>> [%s]", bySelector, fromNode));
+ return fromNode;
+ }
+
+ /**
+ * Used by the {@link #translateCompoundSelector(By, 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
+ * @param index
+ * @return AccessibilityNodeInfo if found else null
+ */
+ private AccessibilityNodeInfo translateReqularSelector(By selector,
+ AccessibilityNodeInfo fromNode) {
+
+ return findNodeRegularRecursive(selector, fromNode, 0);
+ }
+
+ private AccessibilityNodeInfo findNodeRegularRecursive(By 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()) {
+ // TODO: need to remove this or move it under if (DEBUG)
+ Log.d(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(By, 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 originalPattern
+ * @return null of node is not found or if counting mode is true.
+ * See {@link #translateCompoundSelector(By, AccessibilityNodeInfo, boolean)}
+ */
+ private AccessibilityNodeInfo translatePatternSelector(By 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(
+ By subSelector, AccessibilityNodeInfo fromNode, int index, By 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()) {
+ // TODO: need to remove this or move it under 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.getRootAccessibilityNodeInfoInActiveWindow();
+ }
+
+ /**
+ * Last activity to report accessibility events
+ * @return String name of activity
+ */
+ 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();
+ synchronized (mLock) {
+ return mLastPackageName;
+ }
+ }
+
+ 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/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java b/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java
new file mode 100644
index 0000000..90aa4df
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java
@@ -0,0 +1,201 @@
+/*
+ * 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 com.android.internal.util.Predicate;
+
+import android.accessibilityservice.UiTestAutomationBridge;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+class UiAutomatorBridge extends UiTestAutomationBridge {
+
+ private static final String LOGTAG = 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 value is used to wait for the UI to go busy after an action. This has little
+ // bearing on the appearance of test execution speeds. This value is used as a maximum
+ // time to wait for busy state where it is possible to occur much sooner.
+ private static final long WAIT_TIME_FROM_IDLE_TO_BUSY_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
+
+ // Poll time used to check for last accessibility event time
+ private static final long BUSY_STATE_POLL_TIME = 50; //ms
+
+ private final CopyOnWriteArrayList<AccessibilityEventListener> mListeners =
+ new CopyOnWriteArrayList<AccessibilityEventListener>();
+
+ private final Object mLock = new Object();
+
+ private final InteractionController mInteractionController;
+
+ private final QueryController mQueryController;
+
+ private long mLastEventTime = 0;
+ private long mLastOperationTime = 0;
+
+ private volatile boolean mWaitingForEventDelivery;
+
+ public static final long TIMEOUT_ASYNC_PROCESSING = 5000;
+
+ private final LinkedBlockingQueue<AccessibilityEvent> mEventQueue =
+ new LinkedBlockingQueue<AccessibilityEvent>(10);
+
+ public interface AccessibilityEventListener {
+ public void onAccessibilityEvent(AccessibilityEvent event);
+ }
+
+ UiAutomatorBridge() {
+ mInteractionController = new InteractionController(this);
+ mQueryController = new QueryController(this);
+ connect();
+ }
+
+ InteractionController getInteractionController() {
+ return mInteractionController;
+ }
+
+ QueryController getQueryController() {
+ return mQueryController;
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ super.onAccessibilityEvent(event);
+ Log.d(LOGTAG, event.toString());
+ if (mWaitingForEventDelivery) {
+ try {
+ AccessibilityEvent clone = AccessibilityEvent.obtain(event);
+ mEventQueue.offer(clone, TIMEOUT_ASYNC_PROCESSING, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ if (!mWaitingForEventDelivery) {
+ mEventQueue.clear();
+ }
+ }
+ mLastEventTime = SystemClock.uptimeMillis();
+ notifyListeners(event);
+ }
+
+
+ void addAccessibilityEventListener(AccessibilityEventListener listener) {
+ mListeners.add(listener);
+ }
+
+ private void notifyListeners(AccessibilityEvent event) {
+ for (AccessibilityEventListener listener : mListeners) {
+ listener.onAccessibilityEvent(event);
+ }
+ }
+
+ @Override
+ public void waitForIdle(long idleTimeout, long globalTimeout) {
+ long start = SystemClock.uptimeMillis();
+ while ((SystemClock.uptimeMillis() - start) < WAIT_TIME_FROM_IDLE_TO_BUSY_STATE) {
+ if (getLastOperationTime() > getLastEventTime()) {
+ SystemClock.sleep(BUSY_STATE_POLL_TIME);
+ } else {
+ break;
+ }
+ }
+ super.waitForIdle(idleTimeout, globalTimeout);
+ }
+
+ public void waitForIdle() {
+ waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE);
+ }
+
+ public void waitForIdle(long timeout) {
+ waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout);
+ }
+
+ private long getLastEventTime() {
+ synchronized (mLock) {
+ return mLastEventTime;
+ }
+ }
+
+ private long getLastOperationTime() {
+ synchronized (mLock) {
+ return mLastOperationTime;
+ }
+ }
+
+ void setOperationTime() {
+ synchronized (mLock) {
+ mLastOperationTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ void updateEventTime() {
+ synchronized (mLock) {
+ mLastEventTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command,
+ Predicate<AccessibilityEvent> predicate, long timeoutMillis)
+ throws TimeoutException, Exception {
+ // Prepare to wait for an event.
+ mWaitingForEventDelivery = true;
+ // Execute the command.
+ command.run();
+ // Wait for the event.
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ while (true) {
+ // Check if timed out and if not wait.
+ final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
+ final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
+ if (remainingTimeMillis <= 0) {
+ mWaitingForEventDelivery = false;
+ mEventQueue.clear();
+ throw new TimeoutException("Expected event not received within: "
+ + timeoutMillis + " ms.");
+ }
+ AccessibilityEvent event = null;
+ try {
+ event = mEventQueue.poll(remainingTimeMillis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ if (event != null) {
+ if (predicate.apply(event)) {
+ mWaitingForEventDelivery = false;
+ mEventQueue.clear();
+ return event;
+ } else {
+ event.recycle();
+ }
+ }
+ }
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiCollection.java b/uiautomator/library/src/com/android/uiautomator/core/UiCollection.java
new file mode 100644
index 0000000..8b7c3b3
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiCollection.java
@@ -0,0 +1,126 @@
+/*
+ * 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 verification
+ * and/or targeting a sub container by a child's text or description. For example
+ * if a list view contained many list items each in its own LinearLayout, and
+ * the test desired to locate an On/Off switch next to text Wi-Fi so not to be
+ * confused with a switch near text Bluetooth, the test use a UiCollection pointing
+ * at the list view of the items then use {@link #getChildByText(By, String)} for
+ * locating the LinearLayout element containing the text Wi-Fi. The returned UiObject
+ * can further be used to retrieve a child by selector targeting the desired switch and
+ * not other switches that may also be in the list.
+ */
+public class UiCollection extends UiObject {
+
+ public UiCollection(By selector) {
+ super(selector);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiCollection {@link By}
+ * 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 By} 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
+ */
+ public UiObject getChildByDescription(By childPattern, String text)
+ throws UiObjectNotFoundException {
+ 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(By.selector().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 By}
+ * 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 By} 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>
+ */
+ public UiObject getChildByInstance(By childPattern, int instance)
+ throws UiObjectNotFoundException {
+ By patternSelector = By.patternBuilder(getSelector(),
+ By.patternBuilder(childPattern).instance(instance));
+ return new UiObject(patternSelector);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiCollection {@link By}
+ * 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 By} 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
+ */
+ public UiObject getChildByText(By childPattern, String text)
+ throws UiObjectNotFoundException {
+
+ 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(By.selector().text(text));
+ if (item.exists()) {
+ return row;
+ }
+ }
+ }
+ throw new UiObjectNotFoundException("for text= \"" + text + "\"");
+ }
+
+ /**
+ * Count child UI element instances matching the <code>childPattern</code>
+ * argument. The number of elements match returned represent those elements that are
+ * currently visible on the display within the sub hierarchy of this UiCollection {@link By}
+ * selector. Take note that more elements may be present but invisible and are not counted.
+ * @param childPattern is a {@link By} selector that is a pattern to count
+ * @return the number of matched childPattern under the current {@link UiCollection}
+ */
+ public int getChildCount(By childPattern) {
+ By patternSelector = By.patternBuilder(getSelector(), By.patternBuilder(childPattern));
+ return getQueryController().getPatternCount(patternSelector);
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java b/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java
new file mode 100644
index 0000000..c1a867c
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java
@@ -0,0 +1,646 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.core;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Display;
+import android.view.IWindowManager;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.WindowManagerImpl;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.Predicate;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * UiDevice provides access to device wide states. Also provides methods to simulate
+ * pressing hardware buttons such as DPad or the soft buttons such as Home and Menu.
+ */
+public class UiDevice {
+ private static final String LOG_TAG = UiDevice.class.getSimpleName();
+
+ private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
+
+ // store for registered UiWatchers
+ private final HashMap<String, UiWatcher> mWatchers = new HashMap<String, UiWatcher>();
+ private final List<String> mWatchersTriggers = new ArrayList<String>();
+
+ // remember if we're executing in the context of a UiWatcher
+ private boolean mInWatcherContext = false;
+
+ // provides access the {@link QueryController} and {@link InteractionController}
+ private final UiAutomatorBridge mUiAutomationBridge;
+
+ // reference to self
+ private static UiDevice mDevice;
+
+ private Boolean mIsPhone = null;
+
+ private UiDevice() {
+ mUiAutomationBridge = new UiAutomatorBridge();
+ mDevice = this;
+ }
+
+ boolean isInWatcherContext() {
+ return mInWatcherContext;
+ }
+
+ /**
+ * Provides access the {@link QueryController} and {@link InteractionController}
+ * @return {@link UiAutomatorBridge}
+ */
+ UiAutomatorBridge getAutomatorBridge() {
+ return mUiAutomationBridge;
+ }
+ /**
+ * Allow both the direct creation of a UiDevice and retrieving a existing
+ * instance of UiDevice. This helps tests and their libraries to have access
+ * to UiDevice with necessitating having to always pass copies of UiDevice
+ * instances around.
+ * @return UiDevice instance
+ */
+ public static UiDevice getInstance() {
+ if (mDevice == null) {
+ mDevice = new UiDevice();
+ }
+ return mDevice;
+ }
+
+ /**
+ * This forces the return value of {@link #isPhone()} to be a specific device type.
+ * For example, on certain devices the {@link #isPhone} may return true when an application
+ * is actually behaving as if it is on a tablet. For these types of devices, it would be
+ * best if the test forces the issue by invoking this method accordingly.
+ * @param val true for phone behavior else false for all other
+ */
+ public void setTypeAsPhone(boolean val) {
+ mIsPhone = val;
+ }
+
+ /**
+ * Check if the tests are running on a phone screen. This method assumes a
+ * phone is a device that its natural rotation has a height > width or when
+ * rotated it has a width > height. This API is deprecated. Use the UI to
+ * determine the layout. For example if on larger screen devices your app displays
+ * two ListViews but on a small screen one, then count the ListViews to decide. see
+ * {@link UiObject#getMatchesCount()}
+ * @return true if the device has a phone else false
+ */
+ @Deprecated
+ public boolean isPhone() {
+ if(mIsPhone == null) {
+ DisplayMetrics metrics = new DisplayMetrics();
+ Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
+ display.getMetrics(metrics);
+
+ if(isOrientationNatural()) {
+ // we assume a phone has a natural orientation that has height > width
+ if(metrics.heightPixels > metrics.widthPixels)
+ return true;
+ } else {
+ // we assume a phone has a rotated orientation that has height < width
+ if(metrics.heightPixels < metrics.widthPixels)
+ return true;
+ }
+
+ // not a phone
+ return false;
+ }
+
+ return mIsPhone;
+ }
+
+ /**
+ * Check the current device orientation
+ * @return true if in natural orientation
+ */
+ public boolean isOrientationNatural() {
+ Display display = WindowManagerImpl.getDefault().getDefaultDisplay();
+ return display.getRotation() == Surface.ROTATION_0 ||
+ display.getRotation() == Surface.ROTATION_180;
+ }
+
+ /**
+ * Every event received from accessibility may or may not contain text. This
+ * method returns the text from the last UI traversal event received that had text.
+ * This is helpful in web views when the test performs down arrow presses to focus
+ * on different elements inside the web view, the accessibility will fire events
+ * with the text just highlighted. In effect once can read the contents of a
+ * web view this way.
+ * @return text of the last traversal event else an empty string
+ */
+ public String getLastTraversedText() {
+ return mUiAutomationBridge.getQueryController().getLastTraversedText();
+ }
+
+ /**
+ * Helper to clear the text saved of the last accessibility UI traversal event that had
+ * any text in it. See {@link #getLastTraversedText()}.
+ */
+ public void clearLastTraversedText() {
+ mUiAutomationBridge.getQueryController().clearLastTraversedText();
+ }
+
+ /**
+ * Helper method to do a short press on MENU button
+ * @return true if successful else false
+ */
+ public boolean pressMenu() {
+ return pressKeyCode(KeyEvent.KEYCODE_MENU);
+ }
+
+ /**
+ * Helper method to do a short press on BACK button
+ * @return true if successful else false
+ */
+ public boolean pressBack() {
+ return pressKeyCode(KeyEvent.KEYCODE_BACK);
+ }
+
+ /**
+ * Helper method to do a short press on HOME button
+ * @return true if successful else false
+ */
+ public boolean pressHome() {
+ return pressKeyCode(KeyEvent.KEYCODE_HOME);
+ }
+
+ /**
+ * Helper method to do a short press on SEARCH button
+ * @return true if successful else false
+ */
+ public boolean pressSearch() {
+ return pressKeyCode(KeyEvent.KEYCODE_SEARCH);
+ }
+
+ /**
+ * Helper method to do a short press on DOWN button
+ * @return true if successful else false
+ */
+ public boolean pressDPadCenter() {
+ return pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER);
+ }
+
+ /**
+ * Helper method to do a short press on DOWN button
+ * @return true if successful else false
+ */
+ public boolean pressDPadDown() {
+ return pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+
+ /**
+ * Helper method to do a short press on UP button
+ * @return true if successful else false
+ */
+ public boolean pressDPadUp() {
+ return pressKeyCode(KeyEvent.KEYCODE_DPAD_UP);
+ }
+
+ /**
+ * Helper method to do a short press on LEFT button
+ * @return true if successful else false
+ */
+ public boolean pressDPadLeft() {
+ return pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT);
+ }
+
+ /**
+ * Helper method to do a short press on RIGTH button
+ * @return true if successful else false
+ */
+ public boolean pressDPadRight() {
+ return pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT);
+ }
+
+ /**
+ * Helper method to do a short press on DELETE
+ * @return true if successful else false
+ */
+ public boolean pressDelete() {
+ return pressKeyCode(KeyEvent.KEYCODE_DEL);
+ }
+
+ /**
+ * Helper method to do a short press on ENTER
+ * @return true if successful else false
+ */
+ public boolean pressEnter() {
+ return pressKeyCode(KeyEvent.KEYCODE_ENTER);
+ }
+
+ /**
+ * Helper method to do a short press using a key code. See {@link KeyEvent}
+ * @return true if successful else false
+ */
+ public boolean pressKeyCode(int keyCode) {
+ waitForIdle();
+ return mUiAutomationBridge.getInteractionController().sendKey(keyCode, 0);
+ }
+
+ /**
+ * Helper method to do a short press using a key code. See {@link KeyEvent}
+ * @param keyCode See {@link KeyEvent}
+ * @param metaState See {@link KeyEvent}
+ * @return true if successful else false
+ */
+ public boolean pressKeyCode(int keyCode, int metaState) {
+ waitForIdle();
+ return mUiAutomationBridge.getInteractionController().sendKey(keyCode, metaState);
+ }
+
+ /**
+ * Gets the raw width of the display, in pixels. The size is adjusted based
+ * on the current rotation of the display.
+ * @return width in pixels or zero on failure
+ */
+ public int getDisplayWidth() {
+ IWindowManager wm = IWindowManager.Stub.asInterface(
+ ServiceManager.getService(Context.WINDOW_SERVICE));
+ Point p = new Point();
+ try {
+ wm.getDisplaySize(p);
+ } catch (RemoteException e) {
+ return 0;
+ }
+ return p.x;
+ }
+
+ /**
+ * Press recent apps soft key
+ * @return true if successful
+ * @throws RemoteException
+ */
+ public boolean pressRecentApps() throws RemoteException {
+ waitForIdle();
+ final IStatusBarService statusBar = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+
+ if (statusBar != null) {
+ statusBar.toggleRecentApps();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets the raw height of the display, in pixels. The size is adjusted based
+ * on the current rotation of the display.
+ * @return height in pixels or zero on failure
+ */
+ public int getDisplayHeight() {
+ IWindowManager wm = IWindowManager.Stub.asInterface(
+ ServiceManager.getService(Context.WINDOW_SERVICE));
+ Point p = new Point();
+ try {
+ wm.getDisplaySize(p);
+ } catch (RemoteException e) {
+ return 0;
+ }
+ return p.y;
+ }
+
+ /**
+ * Perform a touch at arbitrary coordinates specified by the user
+ * @param x coordinate
+ * @param y coordinate
+ * @return true if the touch succeeded else false
+ */
+ public boolean touch(int x, int y) {
+ if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
+ return (false);
+ }
+ return getAutomatorBridge().getInteractionController().tap(x, y);
+ }
+
+ /**
+ * Performs a swipe from one coordinate to another using the number of steps
+ * to determine smoothness and speed. The more steps the slower and smoother
+ * the swipe will be.
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ * @param steps is the number of move steps sent to the system
+ * @return false if the operation fails or the coordinates are invalid
+ */
+ public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
+ return mUiAutomationBridge.getInteractionController()
+ .scrollSwipe(startX, startY, endX, endY, steps);
+ }
+
+ /**
+ * Performs a swipe between points in the Point array.
+ * @param segments is Point array containing at least one Point object
+ * @param segmentSteps steps to inject between two Points
+ * @return true on success
+ */
+ public boolean swipe(Point[] segments, int segmentSteps) {
+ return mUiAutomationBridge.getInteractionController().swipe(segments, segmentSteps);
+ }
+
+ public void waitForIdle() {
+ waitForIdle(DEFAULT_TIMEOUT_MILLIS);
+ }
+
+ public void waitForIdle(long time) {
+ mUiAutomationBridge.waitForIdle(time);
+ }
+
+ /**
+ * Last activity to report accessibility events
+ * @return String name of activity
+ */
+ public String getCurrentActivityName() {
+ return mUiAutomationBridge.getQueryController().getCurrentActivityName();
+ }
+
+ /**
+ * Last package to report accessibility events
+ * @return String name of package
+ */
+ public String getCurrentPackageName() {
+ return mUiAutomationBridge.getQueryController().getCurrentPackageName();
+ }
+
+
+ /**
+ * Enables the test script to register a condition watcher to be called by
+ * the automation library. The automation library will invoke
+ * {@link UiWatcher#checkForCondition} only when a regular API call is in
+ * retry mode when 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 that may be causing the delay in detecting the
+ * required UI object.
+ * @param name of watcher
+ * @param watcher {@link UiWatcher}
+ */
+ public void registerWatcher(String name, UiWatcher watcher) {
+ if (mInWatcherContext) {
+ throw new IllegalStateException("Cannot register new watcher from within another");
+ }
+ mWatchers.put(name, watcher);
+ }
+
+ /**
+ * Removes a previously registered {@link #registerWatcher(String, UiWatcher)}.
+ * @param name of watcher used when <code>registerWatcher</code> was called.
+ * @throws UiAutomationException
+ */
+ public void removeWatcher(String name) {
+ if (mInWatcherContext) {
+ throw new IllegalStateException("Cannot remove a watcher from within another");
+ }
+ mWatchers.remove(name);
+ }
+
+ /**
+ * Watchers are generally not run unless a certain UI object is not being
+ * found. This will help improve performance of tests until there is a good
+ * reason to check for possible exceptions on the display.<b/><b/> However,
+ * in some cases it may be desirable to force run the watchers. Calling this
+ * method will execute all registered watchers.
+ */
+ public void runWatchers() {
+ if (mInWatcherContext) {
+ return;
+ }
+
+ for (String watcherName : mWatchers.keySet()) {
+ UiWatcher watcher = mWatchers.get(watcherName);
+ if (watcher != null) {
+ try {
+ mInWatcherContext = true;
+ if (watcher.checkForCondition()) {
+ setWatcherTriggered(watcherName);
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Exceuting watcher: " + watcherName, e);
+ } finally {
+ mInWatcherContext = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * If you have used {@link #registerWatcher(String, UiWatcher)} then this
+ * method can be used to reset reported UiWatcher triggers.
+ * A {@link UiWatcher} reports it is triggered by returning true
+ * from its implementation of {@link UiWatcher#checkForCondition()}
+ */
+ public void resetWatcherTriggers() {
+ mWatchersTriggers.clear();
+ }
+
+ /**
+ * If you have used {@link #registerWatcher(String, UiWatcher)} then this
+ * method can be used to check if a specific UiWatcher has ever triggered during the
+ * test. For a {@link UiWatcher} to report it is triggered it needs to return true
+ * from its implementation of {@link UiWatcher#checkForCondition()}
+ */
+ public boolean hasWatcherTriggered(String watcherName) {
+ return mWatchersTriggers.contains(watcherName);
+ }
+
+ /**
+ * If you have used {@link #registerWatcher(String, UiWatcher)} then this
+ * method can be used to check if any of those have ever triggered during the
+ * test. For a {@link UiWatcher} to report it is triggered it needs to return true
+ * from its implementation of {@link UiWatcher#checkForCondition()}
+ */
+ public boolean hasAnyWatcherTriggered() {
+ return mWatchersTriggers.size() > 0;
+ }
+
+ private void setWatcherTriggered(String watcherName) {
+ if (!hasWatcherTriggered(watcherName)) {
+ mWatchersTriggers.add(watcherName);
+ }
+ }
+
+ /**
+ * Check if the device is in its natural orientation. This is determined by
+ * checking whether the orientation is at 0 or 180 degrees.
+ * @return true if it is in natural orientation
+ * @throws RemoteException
+ */
+ public boolean isNaturalRotation() throws RemoteException {
+ return getAutomatorBridge().getInteractionController().isNaturalRotation();
+ }
+
+ /**
+ * Disables the sensors and freezes the device rotation at its
+ * current rotation state.
+ * @throws RemoteException
+ */
+ public void freezeRotation() throws RemoteException {
+ getAutomatorBridge().getInteractionController().freezeRotation();
+ }
+
+ /**
+ * Re-enables the sensors and un-freezes the device rotation
+ * allowing its contents to rotate with the device physical rotation.
+ * @throws RemoteException
+ */
+ public void unfreezeRotation() throws RemoteException {
+ getAutomatorBridge().getInteractionController().unfreezeRotation();
+ }
+
+ /**
+ * 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() throws RemoteException {
+ getAutomatorBridge().getInteractionController().setRotationLeft();
+ }
+
+ /**
+ * 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() throws RemoteException {
+ getAutomatorBridge().getInteractionController().setRotationRight();
+ }
+
+ /**
+ * Check if the device is in its natural orientation. This is determined by
+ * checking whether the orientation is at 0 or 180 degrees.
+ * @return true if it is in natural orientation
+ * @throws RemoteException
+ */
+ public void setRotationNatural() throws RemoteException {
+ getAutomatorBridge().getInteractionController().setRotationNatural();
+ }
+
+ /**
+ * This method simply presses the power button if the screen is OFF else
+ * it does nothing if the screen is already ON. If the screen was OFF and
+ * it just got turned ON, this method will insert a 500ms delay to allow
+ * the device time to wake up and accept input.
+ * @throws RemoteException
+ */
+ public void wakeUp() throws RemoteException {
+ if(getAutomatorBridge().getInteractionController().wakeDevice()) {
+ // sync delay to allow the window manager to start accepting input
+ // after the device is awakened.
+ SystemClock.sleep(500);
+ }
+ }
+
+ /**
+ * Checks the power manager if the screen is ON
+ * @return true if the screen is ON else false
+ * @throws RemoteException
+ */
+ public boolean isScreenOn() throws RemoteException {
+ return getAutomatorBridge().getInteractionController().isScreenOn();
+ }
+
+ /**
+ * This method simply presses the power button if the screen is ON else
+ * it does nothing if the screen is already OFF.
+ * @throws RemoteException
+ */
+ public void sleep() throws RemoteException {
+ getAutomatorBridge().getInteractionController().sleepDevice();
+ }
+
+ /**
+ * Helper method used for debugging to dump the current window's layout hierarchy.
+ * The file root location is /data/local/tmp
+ * @param fileName
+ */
+ public void dumpWindowHierarchy(String fileName) {
+ AccessibilityNodeInfo root =
+ getAutomatorBridge().getQueryController().getAccessibilityRootNode();
+ if(root != null) {
+ AccessibilityNodeInfoDumper.dumpWindowToFile(
+ root, new File(new File(Environment.getDataDirectory(),
+ "local/tmp"), fileName));
+ }
+ }
+
+
+ /**
+ * Waits for a window content update event to occur
+ *
+ * if a package name for window is specified, but current window is not with the same package
+ * name, the function will return immediately
+ *
+ * @param packageName the specified window package name; maybe <code>null</code>, and a window
+ * update from any frontend window will end the wait
+ * @param timeout the timeout for the wait
+ *
+ * @return true if a window update occured, false if timeout has reached or current window is
+ * not the specified package name
+ */
+ public boolean waitForWindowUpdate(final String packageName, long timeout) {
+ if (packageName != null) {
+ if (!packageName.equals(getCurrentPackageName())) {
+ return false;
+ }
+ }
+ Runnable emptyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+ Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
+ @Override
+ public boolean apply(AccessibilityEvent t) {
+ if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
+ return packageName == null || packageName.equals(t.getPackageName());
+ }
+ return false;
+ }
+ };
+ try {
+ getAutomatorBridge().executeCommandAndWaitForAccessibilityEvent(
+ emptyRunnable, checkWindowUpdate, timeout);
+ } catch (TimeoutException e) {
+ return false;
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "waitForWindowUpdate: general exception from bridge", e);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiObject.java b/uiautomator/library/src/com/android/uiautomator/core/UiObject.java
new file mode 100644
index 0000000..9d09332
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiObject.java
@@ -0,0 +1,751 @@
+/*
+ * 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.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * UiObject is designed to be a representation of displayed UI element. It is not in any way
+ * directly bound to a specific UI element. It holds information to find any displayed UI element
+ * that matches its selectors. This means it can be reused on any screen where a UI element
+ * exists to match its selector criteria. This help tests define a single UiObject say for
+ * an "OK" or tool-bar button and use it across many activities.
+ */
+public class UiObject {
+ private static final String LOG_TAG = UiObject.class.getSimpleName();
+ protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000;
+ protected static final long WAIT_FOR_SELECTOR_POLL = 1000;
+ // set a default timeout to 5.5s, since ANR threshold is 5s
+ protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500;
+ protected static final int SWIPE_MARGIN_LIMIT = 5;
+
+ protected By mSelector;
+ protected final UiDevice mDevice;
+ protected final UiAutomatorBridge mUiAutomationBridge;
+
+ /**
+ * Constructs a UiObject that references any UI element that may match the specified
+ * {@link By} selector. UiObject can be pre-constructed and reused across an application
+ * where applicable. A good example is a tool bar and its buttons. A tool bar may remain
+ * visible on the various views the application is displaying but may have different
+ * contextual buttons displayed for each. The same UiObject that describes the tool bar
+ * can be reused.<p/>
+ * It is a good idea in certain cases before using any operations of this UiObject is to
+ * call {@link #exists()} to verify if the UI element matching this object is actually
+ * visible on the screen. This way the test can gracefully handle this situation else
+ * a {@link UiObjectNotFoundException} will be thrown when using this object.
+ * @param selector
+ */
+ public UiObject(By selector) {
+ mUiAutomationBridge = UiDevice.getInstance().getAutomatorBridge();
+ mDevice = UiDevice.getInstance();
+ mSelector = selector;
+ }
+
+ /**
+ * Helper for debugging. During testing a test can dump the selector of the object into
+ * its logs of needed. <code>getSelector().toString();</code>
+ * @return {@link By}
+ */
+ public final By getSelector() {
+ return By.selector(mSelector);
+ }
+
+ /**
+ * Used in test operations to retrieve the {@link QueryController} to translate
+ * a {@link By} selector into an {@link AccessibilityNodeInfo}.
+ * @return {@link QueryController}
+ */
+ protected QueryController getQueryController() {
+ return mUiAutomationBridge.getQueryController();
+ }
+
+ /**
+ * Used in test operations to retrieve the {@link InteractionController} to perform
+ * finger actions such as tapping, swiping or entering text.
+ * @return {@link InteractionController}
+ */
+ protected InteractionController getInteractionController() {
+ return mUiAutomationBridge.getInteractionController();
+ }
+
+ /**
+ * Creates a new UiObject that points at a child UI element of the currently pointed
+ * to element by this object. UI element are considered layout elements as well as UI
+ * widgets. A layout element could have child widgets like buttons and text labels.
+ * @param selector
+ * @return a new UiObject with a new selector. It doesn't test if the object exists.
+ */
+ public UiObject getChild(By selector) throws UiObjectNotFoundException {
+ return new UiObject(By.selector(getSelector().childSelector(selector)));
+ }
+
+ /**
+ * Creates a new UiObject that points at a child UI element of the parent of this object.
+ * Essentially this is starting the search from any one of the siblings UI element of this
+ * element.
+ * @param selector
+ * @return
+ * @throws UiObjectNotFoundException
+ */
+ public UiObject getFromParent(By selector) throws UiObjectNotFoundException {
+ return new UiObject(By.selector(getSelector().fromParent(selector)));
+ }
+
+ /**
+ * Counts the child UI elements immediately under the UI element currently referenced by
+ * this UiObject.
+ * @return the count of child UI elements.
+ * @throws UiObjectNotFoundException
+ */
+ public int getChildCount() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.getChildCount();
+ }
+
+ /**
+ * Helper method to allow for a specific content to become present on the
+ * screen before moving on to do other operations.
+ * @param selector {@link By}
+ * @param timeout in milliseconds
+ * @return AccessibilityNodeInfo if found else null
+ */
+ protected AccessibilityNodeInfo findAccessibilityNodeInfo(long timeout) {
+ AccessibilityNodeInfo node = null;
+ if(UiDevice.getInstance().isInWatcherContext()) {
+ // we will NOT run watchers or do any sort of polling if the
+ // reason we're here is because of a watcher is executing. Watchers
+ // will not have other watchers run for them so they should not block
+ // while they poll for items to become present. We disable polling for them.
+ node = getQueryController().findAccessibilityNodeInfo(getSelector());
+ } else {
+ long startMills = SystemClock.uptimeMillis();
+ long currentMills = 0;
+ while (currentMills <= timeout) {
+ node = getQueryController().findAccessibilityNodeInfo(getSelector());
+ if (node != null) {
+ break;
+ } else {
+ UiDevice.getInstance().runWatchers();
+ }
+ currentMills = SystemClock.uptimeMillis() - startMills;
+ if(timeout > 0) {
+ SystemClock.sleep(WAIT_FOR_SELECTOR_POLL);
+ }
+ }
+ }
+ return node;
+ }
+
+ /**
+ * Perform the action on the UI element that is represented by this object. Also see
+ * {@link #scrollToBeginning(int)}, {@link #scrollToEnd(int)}, {@link #scrollBackward()},
+ * {@link #scrollForward()}. This method will perform the swipe gesture over any
+ * surface. The targeted UI element does not need to have the attribute
+ * <code>scrollable</code> set to <code>true</code> for this operation to be performed.
+ * @param steps indicates the number of injected move steps into the system. More steps
+ * injected the smoother the motion and slower.
+ * @return
+ * @throws UiObjectNotFoundException
+ */
+ public boolean swipeUp(int steps) throws UiObjectNotFoundException {
+ Rect rect = getBounds();
+ 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);
+ }
+
+ /**
+ * Perform the action on the UI element that is represented by this object, Also see
+ * {@link #scrollToBeginning(int)}, {@link #scrollToEnd(int)}, {@link #scrollBackward()},
+ * {@link #scrollForward()}. This method will perform the swipe gesture over any
+ * surface. The targeted UI element does not need to have the attribute
+ * <code>scrollable</code> set to <code>true</code> for this operation to be performed.
+ * @param steps indicates the number of injected move steps into the system. More steps
+ * injected the smoother the motion and slower.
+ * @return
+ * @throws UiObjectNotFoundException
+ */
+ public boolean swipeDown(int steps) throws UiObjectNotFoundException {
+ Rect rect = getBounds();
+ 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);
+ }
+
+ /**
+ * Perform the action on the UI element that is represented by this object. Also see
+ * {@link #scrollToBeginning(int)}, {@link #scrollToEnd(int)}, {@link #scrollBackward()},
+ * {@link #scrollForward()}. This method will perform the swipe gesture over any
+ * surface. The targeted UI element does not need to have the attribute
+ * <code>scrollable</code> set to <code>true</code> for this operation to be performed.
+ * @param steps indicates the number of injected move steps into the system. More steps
+ * injected the smoother the motion and slower.
+ * @return
+ * @throws UiObjectNotFoundException
+ */
+ public boolean swipeLeft(int steps) throws UiObjectNotFoundException {
+ Rect rect = getBounds();
+ 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);
+ }
+
+ /**
+ * Perform the action on the UI element that is represented by this object. Also see
+ * {@link #scrollToBeginning(int)}, {@link #scrollToEnd(int)}, {@link #scrollBackward()},
+ * {@link #scrollForward()}. This method will perform the swipe gesture over any
+ * surface. The targeted UI element does not need to have the attribute
+ * <code>scrollable</code> set to <code>true</code> for this operation to be performed.
+ * @param steps indicates the number of injected move steps into the system. More steps
+ * injected the smoother the motion and slower.
+ * @return
+ * @throws UiObjectNotFoundException
+ */
+ public boolean swipeRight(int steps) throws UiObjectNotFoundException {
+ Rect rect = getBounds();
+ 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);
+ }
+
+ /**
+ * In rare situations, the node hierarchy returned from accessibility will
+ * return items that are slightly OFF the screen (list view contents). This method
+ * validate that the item is visible to avoid click operation failures. It will adjust
+ * the center of the click as much as possible to be within visible bounds to make
+ * the click successful.
+ * @param node
+ * @return the same AccessibilityNodeInfo passed in as argument
+ */
+ private Rect getVisibleBounds(AccessibilityNodeInfo node) {
+ if (node == null) {
+ return null;
+ }
+
+ // targeted node's bounds
+ Rect nodeRect = new Rect();
+ node.getBoundsInScreen(nodeRect);
+
+ // 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 = new Rect();
+ scrollableParentNode.getBoundsInScreen(parentRect);
+ // adjust for partial clipping of targeted by parent node if required
+ nodeRect.intersect(parentRect);
+ return nodeRect;
+ }
+
+ /**
+ * Walk the hierarchy up to find a scrollable parent. A scrollable parent indicates that
+ * this node may be in a content where it is partially visible due to scrolling. its
+ * clickable center maybe invisible and adjustments should be made to the click coordinates.
+ * @param node
+ * @return
+ */
+ 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 tap over the UI element this object represents. </p>
+ * Take note that the UI element directly represented by this UiObject may not have
+ * its attribute <code>clickable</code> set to <code>true</code> and yet still perform
+ * the click successfully. This is because all clicks are performed in the center of the
+ * targeted UI element and if this element is a child or a parent that wraps the clickable
+ * element the operation will still succeed. This is the reason this operation does not
+ * not validate the targeted UI element is clickable or not before operating.
+ * @return true id successful else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean click() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().tap(rect.centerX(), rect.centerY());
+ }
+
+ /**
+ *
+ * Performs a tap over the represented UI element, and expect window transition as a result.</p>
+ *
+ * This method is similar to the plain {@link UiObject#click()}, with an important difference
+ * that the method goes further to expect a window transition as a result of the tap. 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>
+ * This method is intended for reliably handling window transitions that would typically lasts
+ * longer than the usual preset timeouts.
+ *
+ * @return true if the event was triggered, else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean clickAndWaitForNewWindow() throws UiObjectNotFoundException {
+ return clickAndWaitForNewWindow(WAIT_FOR_WINDOW_TMEOUT);
+ }
+
+ /**
+ *
+ * Performs a tap over the represented UI element, and expect window transition as a result.</p>
+ *
+ * This method is similar to the plain {@link UiObject#click()}, with an important difference
+ * that the method goes further to expect a window transition as a result of the tap. 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>
+ * This method is intended for reliably handling window transitions that would typically lasts
+ * longer than the usual preset timeouts.
+ *
+ * @param timeout timeout before giving up on waiting for new window
+ * @return true if the event was triggered, else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().tapAndWaitForNewWindow(
+ rect.centerX(), rect.centerY(), timeout);
+ }
+
+ /**
+ * Click the top and left corner of the UI element.
+ * @return true on success
+ * @throws Exception
+ */
+ public boolean clickTopLeft() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().tap(rect.left + 5, rect.top + 5);
+ }
+
+ /**
+ * Long clicks a UI element pointed to by the {@link By} selector of this object at the
+ * bottom right corner. The duration of what will be considered as a long click is dynamically
+ * retrieved from the system and used in the operation.
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ */
+ public boolean longClickBottomRight() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTap(rect.right - 5, rect.bottom - 5);
+ }
+
+ /**
+ * Click the bottom and right corner of the UI element.
+ * @return true on success
+ * @throws Exception
+ */
+ public boolean clickBottomRight() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().tap(rect.right - 5, rect.bottom - 5);
+ }
+
+ /**
+ * Long clicks a UI element pointed to by the {@link By} selector of this object. The
+ * duration of what will be considered as a long click is dynamically retrieved from the
+ * system and used in the operation.
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ */
+ public boolean longClick() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTap(rect.centerX(), rect.centerY());
+ }
+
+ /**
+ * Long clicks a UI element pointed to by the {@link By} selector of this object at the
+ * top left corner. The duration of what will be considered as a long click is dynamically
+ * retrieved from the system and used in the operation.
+ * @return true if operation was successful
+ * @throws UiObjectNotFoundException
+ */
+ public boolean longClickTopLeft() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ return getInteractionController().longTap(rect.left + 5, rect.top + 5);
+ }
+
+ /**
+ * This function can be used to return the UI element's displayed text. This applies to
+ * UI element that are displaying labels or edit fields.
+ * @return text value of the current node represented by this UiObject
+ * @throws UiObjectNotFoundException if no match could be found
+ */
+ public String getText() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ 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 content-description value set for the UI element. In Accessibility, the
+ * spoken text to speech is usually the <code>text</code> property of the UI element. If that
+ * is not present, then the content-description is spoken. Many UI element such as buttons on
+ * a toolbar may be too small to incorporate a visible text on their surfaces, so in such
+ * cases, these UI elements must have their content-description fields populated to describe
+ * them when accessibility is active.
+ * @return value of node attribute "content_desc"
+ * @throws UiObjectNotFoundException
+ */
+ public String getContentDescription() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return safeStringReturn(node.getContentDescription());
+ }
+
+ /**
+ * First this function clears the existing text from the field. If this is not the intended
+ * behavior, do a {@link #getText()} first, modify the text and then use this function.
+ * The {@link By} selector of this object MUST be pointing directly at a UI element that
+ * can accept edits. The way this method works is by first performing a {@link #click()}
+ * on the edit field to set focus then it begins injecting the content of the text param
+ * into the system. Since the targeted field is in focus, the text contents should be
+ * inserted into the field.<p/>
+ * @param text
+ * @return true if operation is successful
+ * @throws UiObjectNotFoundException
+ */
+ public boolean setText(String text) throws UiObjectNotFoundException {
+ clearTextField();
+ return getInteractionController().sendText(text);
+ }
+
+ /**
+ * The object targeted must be an edit field capable of performing text insert. This
+ * method sets focus at the left edge of the field and long presses to select
+ * existing text. It will then follow that with delete press. Note: It is possible
+ * that not all the text is selected especially if the text contained separators
+ * such as spaces, slashes, at signs etc... The function will attempt to use the
+ * Select-All option if one is displayed to ensure full text selection.
+ * @throws UiObjectNotFoundException
+ */
+ public void clearTextField() throws UiObjectNotFoundException {
+ // long click left + center
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ Rect rect = getVisibleBounds(node);
+ getInteractionController().longTap(rect.left + 20, rect.centerY());
+ // check if the edit menu is open
+ UiObject selectAll = new UiObject(By.selector().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 item pointed to by this object's selector is currently checked. <p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ */
+ public boolean isChecked() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isChecked();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector is currently selected.<p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isSelected() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isSelected();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector can be checked and unchecked. <p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isCheckable() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isCheckable();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector with text is currently
+ * not grayed out. <p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isEnabled() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isEnabled();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector with text responds to
+ * clicks <p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isClickable() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isClickable();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector with text is currently
+ * focused. Focused objects will receive key stroke events when key events
+ * are fired
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isFocused() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isFocused();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector with text is capable of receiving
+ * focus. <p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isFocusable() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isFocusable();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector with text can be scrolled.<p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isScrollable() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isScrollable();
+ }
+
+ /**
+ * Check if item pointed to by this object's selector responds to long clicks.<p/>
+ * Take note that the {@link By} selector specified for this UiObjecy must be pointing
+ * directly at the element you wish to query for this attribute as this UiObject may be
+ * using {@link By} selectors that may be pointing at a parent element of the element
+ * you're querying while still providing the desired functionality in terms of coordinates
+ * for taps and swipes.
+ * @return true if it is else false
+ * @throws UiObjectNotFoundException
+ */
+ public boolean isLongClickable() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return node.isLongClickable();
+ }
+
+ /**
+ * This method retrieves the package name of the currently displayed content on the screen.
+ * This can be helpful when verifying that the expected package is on the screen before
+ * proceeding with further test operations.
+ * @return String package name
+ * @throws UiObjectNotFoundException
+ */
+ public String getPackageName() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return safeStringReturn(node.getPackageName());
+ }
+
+ /**
+ * Reports the absolute visible screen bounds of the object. If a portion of the UI element
+ * is visible, only the bounds of the visible portion of the UI element are reported. This
+ * becomes important when using bounds to calculate exact coordinates for tapping the element.
+ * @return Rect
+ * @throws UiObjectNotFoundException
+ */
+ public Rect getBounds() throws UiObjectNotFoundException {
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ return getVisibleBounds(node);
+ }
+
+ /**
+ * This method will wait for a UI element to become visible on the display. It
+ * can be used for situations where the content to be selected is not yet displayed
+ * and the time it will be present is unknown.
+ * @param timeout
+ * @return true if the UI element exists else false for timeout while waiting
+ */
+ public boolean waitForExists(long timeout) {
+ if(findAccessibilityNodeInfo(timeout) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Helper to wait for a specified object to no longer be detectable. This can be
+ * useful when having to wait for a progress dialog to finish.
+ * @param timeout
+ * @return true if gone before timeout else false for still present at timeout
+ */
+ public boolean waitUntilGone(long 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;
+ }
+
+ /**
+ * This methods performs a {@link #waitForExists(long)} with zero timeout. This
+ * basically returns immediately whether the UI element represented by this UiObject
+ * exists or not. If you need to wait longer for this UI element, then see
+ * {@link #waitForExists(long)}.
+ * @return true if the UI element represented by this UiObject does exist
+ */
+ public boolean exists() {
+ return waitForExists(0);
+ }
+
+ private String safeStringReturn(CharSequence cs) {
+ if(cs == null)
+ return "";
+ return cs.toString();
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java b/uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java
new file mode 100644
index 0000000..4610160
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java
@@ -0,0 +1,38 @@
+/*
+ * 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 By} selector could not be matched
+ * to any UI element displayed.
+ */
+public class UiObjectNotFoundException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public UiObjectNotFoundException(String msg) {
+ super(msg);
+ }
+
+ public UiObjectNotFoundException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public UiObjectNotFoundException(Throwable throwable) {
+ super(throwable);
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java b/uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java
new file mode 100644
index 0000000..7df4195
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java
@@ -0,0 +1,475 @@
+/*
+ * 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} however this class provides additional functionality
+ * where the tests need to deal with scrollable contents or desire to enumerate lists of
+ * items. This calls can perform automatic searches within a scrollable container. Whether
+ * the content scrolls vertically or horizontally can be set by calling
+ * {@link #setAsVerticalList()} which is the default, or {@link #setAsHorizontalList()}.
+ */
+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
+ protected boolean mIsVerticalList = true;
+
+ private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT;
+
+ /**
+ * UiScrollable is a {@link UiCollection} and as such requires a {@link By} selector to identify
+ * the UI element it represents. In the case of UiScrollable, the selector specified is
+ * considered a container where further calls to enumerate or find children will be performed
+ * in.
+ * @param container a {@link By} selector
+ */
+ public UiScrollable(By 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 when performing scroll search
+ */
+ public void setAsVerticalList() {
+ mIsVerticalList = true;
+ }
+
+ /**
+ * Set the direction of swipes when performing scroll search
+ */
+ public void setAsHorizontalList() {
+ mIsVerticalList = false;
+ }
+
+ /**
+ * Used privately when performing swipe searches to decide if an element has become
+ * visible or not.
+ * @param selector
+ * @return true if found else false
+ */
+ protected boolean exists(By selector) {
+ if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiScrollable {@link By}
+ * 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>
+ * By default this operation will perform scroll search while attempting to find the
+ * UI element.
+ * See {@link #getChildByDescription(By, String, boolean)}
+ * @param childPattern {@link By} 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
+ */
+ @Override
+ public UiObject getChildByDescription(By childPattern, String text)
+ throws UiObjectNotFoundException {
+ return getChildByDescription(childPattern, text, true);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiScrollable {@link By}
+ * 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.
+ * @param childPattern {@link By} selector of the child pattern to match and return
+ * @param text String may be a partial match for the content-description of a child element.
+ * @param allowScrollSearch set to true if scrolling is allowed
+ * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
+ * @throws UiObjectNotFoundException
+ */
+ public UiObject getChildByDescription(By childPattern, String text, boolean allowScrollSearch)
+ throws UiObjectNotFoundException {
+ if (text != null) {
+ if (allowScrollSearch) {
+ scrollIntoView(By.selector().descriptionContains(text));
+ }
+ return super.getChildByDescription(childPattern, text);
+ }
+ throw new UiObjectNotFoundException("for description= \"" + text + "\"");
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiScrollable {@link By}
+ * selector. It looks for any child matching the <code>childPattern</code> argument and
+ * return 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 By} 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>
+ */
+ @Override
+ public UiObject getChildByInstance(By childPattern, int instance)
+ throws UiObjectNotFoundException {
+ By patternSelector = By.patternBuilder(getSelector(),
+ By.patternBuilder(childPattern).instance(instance));
+ return new UiObject(patternSelector);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiScrollable {@link By}
+ * 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>
+ * By default this operation will perform scroll search while attempting to find the UI
+ * element.
+ * See {@link #getChildByText(By, String, boolean)}
+ * @param childPattern {@link By} 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
+ */
+ @Override
+ public UiObject getChildByText(By childPattern, String text)
+ throws UiObjectNotFoundException {
+ return getChildByText(childPattern, text, true);
+ }
+
+ /**
+ * Searches for child UI element within the constraints of this UiScrollable {@link By}
+ * 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 the 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.
+ * @param childPattern {@link By} selector of the child pattern to match and return
+ * @param text String of the identifying child contents of of the <code>childPattern</code>
+ * @param allowScrollSearch set to true if scrolling is allowed
+ * @return {@link UiObject} pointing at and instance of <code>childPattern</code>
+ * @throws UiObjectNotFoundException
+ */
+ public UiObject getChildByText(By childPattern, String text, boolean allowScrollSearch)
+ throws UiObjectNotFoundException {
+
+ if (text != null) {
+ if (allowScrollSearch) {
+ scrollIntoView(By.selector().text(text));
+ }
+ return super.getChildByText(childPattern, text);
+ }
+ throw new UiObjectNotFoundException("for text= \"" + text + "\"");
+ }
+
+ /**
+ * Performs a swipe Up on the associated UI element until the requested content-description
+ * is found or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
+ * @param text to look for anywhere within the contents of this scrollable.
+ * @return true if item us found else false
+ */
+ public boolean scrollDescriptionIntoView(String text) {
+ return scrollIntoView(By.selector().description(text));
+ }
+
+ /**
+ * Perform a scroll search for a UI element matching the {@link By} selector argument. Also
+ * see {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}.
+ * @param selector {@link By} selector
+ * @return true if the item was found and now is in view else false
+ */
+ public boolean scrollIntoView(By selector) {
+ // if we happen to be on top of the text we want then return here
+ if (exists(getSelector().childSelector(selector))) {
+ return (true);
+ } else {
+ // we will need to reset the search from the beginning to start search
+ scrollToBeginning(mMaxSearchSwipes);
+ if (exists(getSelector().childSelector(selector))) {
+ return (true);
+ }
+ for (int x = 0; x < mMaxSearchSwipes; x++) {
+ if(!scrollForward()) {
+ return false;
+ }
+
+ if(exists(getSelector().childSelector(selector))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Performs a swipe up on the associated display element until the requested text
+ * appears or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)}
+ * @param text to look for
+ * @return true if item us found else false
+ */
+ public boolean scrollTextIntoView(String text) {
+ return scrollIntoView(By.selector().text(text));
+ }
+
+ /**
+ * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
+ * use an arguments that specifies if scrolling is allowed while searching for the UI element.
+ * The number of scrolls allowed to perform a search can be modified by this method.
+ * The current value can be read by calling {@link #getMaxSearchSwipes()}
+ * @param swipes
+ */
+ public void setMaxSearchSwipes(int swipes) {
+ mMaxSearchSwipes = swipes;
+ }
+
+ /**
+ * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)}
+ * use an arguments that specifies if scrolling is allowed while searching for the UI element.
+ * The number of scrolls currently allowed to perform a search can be read by this method.
+ * See {@link #setMaxSearchSwipes(int)}
+ * @return max value of the number of swipes currently allowed during a scroll search
+ */
+ public int getMaxSearchSwipes() {
+ return mMaxSearchSwipes;
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling
+ *
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean flingForward() {
+ return scrollForward(FLING_STEPS);
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll
+ *
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean scrollForward() {
+ return scrollForward(SCROLL_STEPS);
+ }
+
+ /**
+ * Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()}
+ * default) then the swipes will be executed from the bottom to top. If this list is set
+ * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
+ * the right to left.
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean scrollForward(int steps) {
+ Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector());
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ // Object Not Found
+ return false;
+ }
+ 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);
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a fling
+ *
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean flingBackward() {
+ return scrollBackward(FLING_STEPS);
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a regular scroll
+ *
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean scrollBackward() {
+ return scrollBackward(SCROLL_STEPS);
+ }
+
+ /**
+ * Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()}
+ * default) then the swipes will be executed from the top to bottom. If this list is set
+ * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from
+ * the left to right.
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true if scrolled and false if can't scroll anymore
+ */
+ public boolean scrollBackward(int steps) {
+ Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector());
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ if(node == null) {
+ // Object Not Found
+ return false;
+ }
+ 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 UI element. The beginning could be the top most
+ * in case of vertical lists or the left most in case of horizontal lists.
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true on scrolled else false
+ */
+ public boolean scrollToBeginning(int maxSwipes, int 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;
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with regular scroll
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ */
+ public boolean scrollToBeginning(int maxSwipes) {
+ return scrollToBeginning(maxSwipes, SCROLL_STEPS);
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with fling
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ */
+ public boolean flingToBeginning(int maxSwipes) {
+ return scrollToBeginning(maxSwipes, FLING_STEPS);
+ }
+
+ /**
+ * Scrolls to the end of a scrollable UI element. The end could be the bottom most
+ * in case of vertical controls or the right most for horizontal controls
+ *
+ * @param steps use steps to control the speed, so that it may be a scroll, or fling
+ * @return true on scrolled else false
+ */
+ public boolean scrollToEnd(int maxSwipes, int steps) {
+ // protect against potential hanging and return after preset attempts
+ for(int x = 0; x < maxSwipes; x++) {
+ if(!scrollForward(steps)) {
+ break;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with regular scroll
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ */
+ public boolean scrollToEnd(int maxSwipes) {
+ return scrollToEnd(maxSwipes, SCROLL_STEPS);
+ }
+
+ /**
+ * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with fling
+ *
+ * @param maxSwipes
+ * @return true on scrolled else false
+ */
+ public boolean flingToEnd(int maxSwipes) {
+ return scrollToEnd(maxSwipes, FLING_STEPS);
+ }
+
+ public double getSwipeDeadZonePercentage() {
+ return mSwipeDeadZonePercentage;
+ }
+
+ public void setSwipeDeadZonePercentage(double swipeDeadZonePercentage) {
+ mSwipeDeadZonePercentage = swipeDeadZonePercentage;
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java b/uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java
new file mode 100644
index 0000000..d241cc5
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+public interface UiWatcher {
+ public boolean checkForCondition();
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java b/uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java
new file mode 100644
index 0000000..350b3dd
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java
@@ -0,0 +1,35 @@
+/*
+ * 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.testrunner;
+
+import android.os.Bundle;
+
+/**
+ * Provides auxiliary support for running test cases
+ *
+ */
+public interface IAutomationSupport {
+
+ /**
+ * Allows the running test cases to send out interim status
+ *
+ * @param bundle status report, consisting of key value pairs
+ *
+ */
+ public void sendStatus(int resultCode, Bundle status);
+
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java b/uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java
new file mode 100644
index 0000000..f8dd622
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java
@@ -0,0 +1,141 @@
+/*
+ * 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.testrunner;
+
+import junit.framework.TestCase;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A convenient class that encapsulates functions for adding test classes
+ *
+ */
+public class TestCaseCollector {
+
+ private ClassLoader mClassLoader;
+ private List<TestCase> mTestCases;
+ private TestCaseFilter mFilter;
+
+ public TestCaseCollector(ClassLoader classLoader, TestCaseFilter filter) {
+ mClassLoader = classLoader;
+ mTestCases = new ArrayList<TestCase>();
+ mFilter = filter;
+ }
+
+ /**
+ * Adds classes to test by providing a list of class names in string
+ *
+ * The class name may be in "<class name>#<method name>" format
+ *
+ * @param classNames class must be subclass of {@link UiAutomatorTestCase}
+ * @throws ClassNotFoundException
+ */
+ public void addTestClasses(List<String> classNames) throws ClassNotFoundException {
+ for (String className : classNames) {
+ addTestClass(className);
+ }
+ }
+
+ /**
+ * Adds class to test by providing class name in string.
+ *
+ * The class name may be in "<class name>#<method name>" format
+ *
+ * @param classNames classes must be subclass of {@link UiAutomatorTestCase}
+ * @throws ClassNotFoundException
+ */
+ public void addTestClass(String className) throws ClassNotFoundException {
+ int hashPos = className.indexOf('#');
+ String methodName = null;
+ if (hashPos != -1) {
+ methodName = className.substring(hashPos + 1);
+ className = className.substring(0, hashPos);
+ }
+ addTestClass(className, methodName);
+ }
+
+ /**
+ * Adds class to test by providing class name and method name in separate strings
+ *
+ * @param className class must be subclass of {@link UiAutomatorTestCase}
+ * @param methodName may be null, in which case all "public void testNNN(void)" functions
+ * will be added
+ * @throws ClassNotFoundException
+ */
+ public void addTestClass(String className, String methodName) throws ClassNotFoundException {
+ Class<?> clazz = mClassLoader.loadClass(className);
+ if (methodName != null) {
+ addSingleTestMethod(clazz, methodName);
+ } else {
+ Method[] methods = clazz.getMethods();
+ for (Method method : methods) {
+ if (mFilter.accept(method)) {
+ addSingleTestMethod(clazz, method.getName());
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the list of added test cases so far
+ *
+ * @return
+ */
+ public List<TestCase> getTestCases() {
+ return Collections.unmodifiableList(mTestCases);
+ }
+
+ protected void addSingleTestMethod(Class<?> clazz, String method) {
+ if (!(mFilter.accept(clazz))) {
+ throw new RuntimeException("Test class must be derived from UiAutomatorTestCase");
+ }
+ try {
+ TestCase testCase = (TestCase) clazz.newInstance();
+ testCase.setName(method);
+ mTestCases.add(testCase);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Could not instantiate test class. Class: "
+ + clazz.getName());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Could not access test class. Class: " + clazz.getName());
+ }
+
+ }
+
+ /**
+ * Determine if a class and its method should be accepted into test suite
+ *
+ */
+ public interface TestCaseFilter {
+
+ /**
+ * Determine that based on the method signature, if it can be accepted
+ * @param method
+ */
+ public boolean accept(Method method);
+
+ /**
+ * Determine that based on the class type, if it can be accepted
+ * @param clazz
+ * @return
+ */
+ public boolean accept(Class<?> clazz);
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
new file mode 100644
index 0000000..d440a9e
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
@@ -0,0 +1,142 @@
+/*
+ * 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.testrunner;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.view.IInputMethodManager;
+import com.android.uiautomator.core.UiDevice;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * UI automation test should extend this class. This class provides access
+ * to the following:
+ * {@link UiDevice} instance
+ * {@link Bundle} for command line parameters.
+ */
+public class UiAutomatorTestCase extends TestCase {
+
+ private static final String DISABLE_IME = "disable_ime";
+ private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime";
+ private UiDevice mUiDevice;
+ private Bundle mParams;
+ private IAutomationSupport mAutomationSupport;
+ private boolean mShouldDisableIme = false;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mShouldDisableIme = "true".equals(mParams.getString(DISABLE_IME));
+ if (mShouldDisableIme) {
+ setDummyIme();
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mShouldDisableIme) {
+ restoreActiveIme();
+ }
+ super.tearDown();
+ }
+
+ /**
+ * Get current instance of {@link UiDevice}. Works similar to calling the static
+ * {@link UiDevice#getInstance()} from anywhere in the test classes.
+ */
+ public UiDevice getUiDevice() {
+ return mUiDevice;
+ }
+
+ /**
+ * Get command line parameters. On the command line when passing <code>-e key value</code>
+ * pairs, the {@link Bundle} will have the key value pairs conveniently available to the
+ * tests.
+ */
+ public Bundle getParams() {
+ return mParams;
+ }
+
+ /**
+ * Provides support for running tests to report interim status
+ *
+ * @return
+ */
+ public IAutomationSupport getAutomationSupport() {
+ return mAutomationSupport;
+ }
+
+ /**
+ * package private
+ * @param uiDevice
+ */
+ void setUiDevice(UiDevice uiDevice) {
+ mUiDevice = uiDevice;
+ }
+
+ /**
+ * package private
+ * @param params
+ */
+ void setParams(Bundle params) {
+ mParams = params;
+ }
+
+ void setAutomationSupport(IAutomationSupport automationSupport) {
+ mAutomationSupport = automationSupport;
+ }
+
+ /**
+ * Calls {@link SystemClock#sleep(long)} to sleep
+ * @param ms is in milliseconds.
+ */
+ public void sleep(long ms) {
+ SystemClock.sleep(ms);
+ }
+
+ protected void setDummyIme() throws RemoteException {
+ IInputMethodManager im = IInputMethodManager.Stub.asInterface(ServiceManager
+ .getService(Context.INPUT_METHOD_SERVICE));
+ List<InputMethodInfo> infos = im.getInputMethodList();
+ String id = null;
+ for (InputMethodInfo info : infos) {
+ if (DUMMY_IME_PACKAGE.equals(info.getComponent().getPackageName())) {
+ id = info.getId();
+ }
+ }
+ if (id == null) {
+ throw new RuntimeException(String.format(
+ "Required testing fixture missing: IME package (%s)", DUMMY_IME_PACKAGE));
+ }
+ im.setInputMethod(null, id);
+ }
+
+ protected void restoreActiveIme() throws RemoteException {
+ // TODO: figure out a way to restore active IME
+ // Currently retrieving active IME requires querying secure settings provider, which is hard
+ // to do without a Context; so the caveat here is that to make the post test device usable,
+ // the active IME needs to be manually switched.
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java
new file mode 100644
index 0000000..7f47e9a
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java
@@ -0,0 +1,41 @@
+/*
+ * 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.testrunner;
+
+import com.android.uiautomator.testrunner.TestCaseCollector.TestCaseFilter;
+
+import java.lang.reflect.Method;
+
+/**
+ * A {@link TestCaseFilter} that accepts testFoo methods and {@link UiAutomatorTestCase} classes
+ *
+ */
+public class UiAutomatorTestCaseFilter implements TestCaseFilter {
+
+ @Override
+ public boolean accept(Method method) {
+ return ((method.getParameterTypes().length == 0) &&
+ (method.getName().startsWith("test")) &&
+ (method.getReturnType().getSimpleName().equals("void")));
+ }
+
+ @Override
+ public boolean accept(Class<?> clazz) {
+ return UiAutomatorTestCase.class.isAssignableFrom(clazz);
+ }
+
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
new file mode 100644
index 0000000..ae2d0ac
--- /dev/null
+++ b/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
@@ -0,0 +1,332 @@
+/*
+ * 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.testrunner;
+
+import android.app.Activity;
+import android.app.IInstrumentationWatcher;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.IBinder;
+import android.test.RepetitiveTest;
+import android.util.Log;
+
+import com.android.uiautomator.core.UiDevice;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestListener;
+import junit.framework.TestResult;
+import junit.runner.BaseTestRunner;
+import junit.textui.ResultPrinter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+public class UiAutomatorTestRunner {
+
+ private static final String LOGTAG = UiAutomatorTestRunner.class.getSimpleName();
+ private static final int EXIT_OK = 0;
+ private static final int EXIT_EXCEPTION = -1;
+
+ private boolean mDebug;
+ private Bundle mParams = null;
+ private UiDevice mUiDevice;
+ private List<String> mTestClasses = null;
+ private FakeInstrumentationWatcher mWatcher = new FakeInstrumentationWatcher();
+ private IAutomationSupport mAutomationSupport = new IAutomationSupport() {
+ @Override
+ public void sendStatus(int resultCode, Bundle status) {
+ mWatcher.instrumentationStatus(null, resultCode, status);
+ }
+ };
+ private List<TestListener> mTestListeners = new ArrayList<TestListener>();
+
+ public void run(List<String> testClasses, Bundle params, boolean debug) {
+ Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ Log.e(LOGTAG, "uncaught exception", ex);
+ Bundle results = new Bundle();
+ results.putString("shortMsg", ex.getClass().getName());
+ results.putString("longMsg", ex.getMessage());
+ mWatcher.instrumentationFinished(null, 0, results);
+ // bailing on uncaught exception
+ System.exit(EXIT_EXCEPTION);
+ }
+ });
+
+ mTestClasses = testClasses;
+ mParams = params;
+ mDebug = debug;
+ start();
+ System.exit(EXIT_OK);
+ }
+
+ /**
+ * Called after all test classes are in place, ready to test
+ */
+ protected void start() {
+ TestCaseCollector collector = getTestCaseCollector(this.getClass().getClassLoader());
+ try {
+ collector.addTestClasses(mTestClasses);
+ } catch (ClassNotFoundException e) {
+ // will be caught by uncaught handler
+ throw new RuntimeException(e.getMessage(), e);
+ }
+ if (mDebug) {
+ Debug.waitForDebugger();
+ }
+ mUiDevice = UiDevice.getInstance();
+ List<TestCase> testCases = collector.getTestCases();
+ Bundle testRunOutput = new Bundle();
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ PrintStream writer = new PrintStream(byteArrayOutputStream);
+ try {
+ StringResultPrinter resultPrinter = new StringResultPrinter(writer);
+
+ TestResult testRunResult = new TestResult();
+ // add test listeners
+ testRunResult.addListener(new WatcherResultPrinter(testCases.size()));
+ testRunResult.addListener(resultPrinter);
+ // add all custom listeners
+ for (TestListener listener : mTestListeners) {
+ testRunResult.addListener(listener);
+ }
+ long startTime = System.currentTimeMillis();
+
+ // run tests for realz!
+ for (TestCase testCase : testCases) {
+ prepareTestCase(testCase);
+ testCase.run(testRunResult);
+ }
+ long runTime = System.currentTimeMillis() - startTime;
+
+ resultPrinter.print2(testRunResult, runTime);
+ } catch (Throwable t) {
+ // catch all exceptions so a more verbose error message can be outputted
+ writer.println(String.format("Test run aborted due to unexpected exception: %s",
+ t.getMessage()));
+ t.printStackTrace(writer);
+ } finally {
+ testRunOutput.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+ String.format("\nTest results for %s=%s",
+ getClass().getSimpleName(),
+ byteArrayOutputStream.toString()));
+ writer.close();
+ mAutomationSupport.sendStatus(Activity.RESULT_OK, testRunOutput);
+ }
+ }
+
+ // copy & pasted from com.android.commands.am.Am
+ private class FakeInstrumentationWatcher implements IInstrumentationWatcher {
+
+ private boolean mRawMode = true;
+
+ @Override
+ public IBinder asBinder() {
+ throw new UnsupportedOperationException("I'm just a fake!");
+ }
+
+ @Override
+ public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) {
+ synchronized (this) {
+ // pretty printer mode?
+ String pretty = null;
+ if (!mRawMode && results != null) {
+ pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
+ }
+ if (pretty != null) {
+ System.out.print(pretty);
+ } else {
+ if (results != null) {
+ for (String key : results.keySet()) {
+ System.out.println("INSTRUMENTATION_STATUS: " + key + "="
+ + results.get(key));
+ }
+ }
+ System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
+ }
+ notifyAll();
+ }
+ }
+
+ @Override
+ public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) {
+ synchronized (this) {
+ // pretty printer mode?
+ String pretty = null;
+ if (!mRawMode && results != null) {
+ pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
+ }
+ if (pretty != null) {
+ System.out.println(pretty);
+ } else {
+ if (results != null) {
+ for (String key : results.keySet()) {
+ System.out.println("INSTRUMENTATION_RESULT: " + key + "="
+ + results.get(key));
+ }
+ }
+ System.out.println("INSTRUMENTATION_CODE: " + resultCode);
+ }
+ notifyAll();
+ }
+ }
+ }
+
+ // Copy & pasted from InstrumentationTestRunner.WatcherResultPrinter
+ private class WatcherResultPrinter implements TestListener {
+
+ private static final String REPORT_KEY_NUM_TOTAL = "numtests";
+ private static final String REPORT_KEY_NAME_CLASS = "class";
+ private static final String REPORT_KEY_NUM_CURRENT = "current";
+ private static final String REPORT_KEY_NAME_TEST = "test";
+ private static final String REPORT_KEY_NUM_ITERATIONS = "numiterations";
+ private static final String REPORT_VALUE_ID = "UiAutomatorTestRunner";
+ private static final String REPORT_KEY_STACK = "stack";
+
+ private static final int REPORT_VALUE_RESULT_START = 1;
+ private static final int REPORT_VALUE_RESULT_ERROR = -1;
+ private static final int REPORT_VALUE_RESULT_FAILURE = -2;
+
+ private final Bundle mResultTemplate;
+ Bundle mTestResult;
+ int mTestNum = 0;
+ int mTestResultCode = 0;
+ String mTestClass = null;
+
+ public WatcherResultPrinter(int numTests) {
+ mResultTemplate = new Bundle();
+ mResultTemplate.putString(Instrumentation.REPORT_KEY_IDENTIFIER, REPORT_VALUE_ID);
+ mResultTemplate.putInt(REPORT_KEY_NUM_TOTAL, numTests);
+ }
+
+ /**
+ * send a status for the start of a each test, so long tests can be seen
+ * as "running"
+ */
+ @Override
+ public void startTest(Test test) {
+ String testClass = test.getClass().getName();
+ String testName = ((TestCase) test).getName();
+ mTestResult = new Bundle(mResultTemplate);
+ mTestResult.putString(REPORT_KEY_NAME_CLASS, testClass);
+ mTestResult.putString(REPORT_KEY_NAME_TEST, testName);
+ mTestResult.putInt(REPORT_KEY_NUM_CURRENT, ++mTestNum);
+ // pretty printing
+ if (testClass != null && !testClass.equals(mTestClass)) {
+ mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+ String.format("\n%s:", testClass));
+ mTestClass = testClass;
+ } else {
+ mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "");
+ }
+
+ Method testMethod = null;
+ try {
+ testMethod = test.getClass().getMethod(testName);
+ // Report total number of iterations, if test is repetitive
+ if (testMethod.isAnnotationPresent(RepetitiveTest.class)) {
+ int numIterations = testMethod.getAnnotation(RepetitiveTest.class)
+ .numIterations();
+ mTestResult.putInt(REPORT_KEY_NUM_ITERATIONS, numIterations);
+ }
+ } catch (NoSuchMethodException e) {
+ // ignore- the test with given name does not exist. Will be
+ // handled during test
+ // execution
+ }
+
+ mAutomationSupport.sendStatus(REPORT_VALUE_RESULT_START, mTestResult);
+ mTestResultCode = 0;
+ }
+
+ @Override
+ public void addError(Test test, Throwable t) {
+ mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t));
+ mTestResultCode = REPORT_VALUE_RESULT_ERROR;
+ // pretty printing
+ mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+ String.format("\nError in %s:\n%s",
+ ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t)));
+ }
+
+ @Override
+ public void addFailure(Test test, AssertionFailedError t) {
+ mTestResult.putString(REPORT_KEY_STACK, BaseTestRunner.getFilteredTrace(t));
+ mTestResultCode = REPORT_VALUE_RESULT_FAILURE;
+ // pretty printing
+ mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT,
+ String.format("\nFailure in %s:\n%s",
+ ((TestCase)test).getName(), BaseTestRunner.getFilteredTrace(t)));
+ }
+
+ @Override
+ public void endTest(Test test) {
+ if (mTestResultCode == 0) {
+ mTestResult.putString(Instrumentation.REPORT_KEY_STREAMRESULT, ".");
+ }
+ mAutomationSupport.sendStatus(mTestResultCode, mTestResult);
+ }
+
+ }
+
+ // copy pasted from InstrumentationTestRunner
+ private class StringResultPrinter extends ResultPrinter {
+
+ public StringResultPrinter(PrintStream writer) {
+ super(writer);
+ }
+
+ synchronized void print2(TestResult result, long runTime) {
+ printHeader(runTime);
+ printFooter(result);
+ }
+ }
+
+ protected TestCaseCollector getTestCaseCollector(ClassLoader classLoader) {
+ return new TestCaseCollector(classLoader, new UiAutomatorTestCaseFilter());
+ }
+
+ protected void addTestListener(TestListener listener) {
+ if (!mTestListeners.contains(listener)) {
+ mTestListeners.add(listener);
+ }
+ }
+
+ protected void removeTestListener(TestListener listener) {
+ mTestListeners.remove(listener);
+ }
+
+ /**
+ * subclass may override this method to perform further preparation
+ *
+ * @param testCase
+ */
+ protected void prepareTestCase(TestCase testCase) {
+ ((UiAutomatorTestCase)testCase).setAutomationSupport(mAutomationSupport);
+ ((UiAutomatorTestCase)testCase).setUiDevice(mUiDevice);
+ ((UiAutomatorTestCase)testCase).setParams(mParams);
+ }
+}