am 5fb28ffc: am 2845dc24: Merge "Add support for JUnit4 Parameterized tests"
* commit '5fb28ffc233c0d23e0a211f30ac2f2b9a1aafbb1':
Add support for JUnit4 Parameterized tests
diff --git a/.gitignore b/.gitignore
index 3a5f35d..aaf6b34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,7 +3,6 @@
*.class
bin/
*.properties
-*.project
*.cproject
*.settings
*.pyc
diff --git a/androidtestlib/src/com/android/test/BundleTest.java b/androidtestlib/src/com/android/test/BundleTest.java
new file mode 100644
index 0000000..f989312
--- /dev/null
+++ b/androidtestlib/src/com/android/test/BundleTest.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test;
+
+import android.os.Bundle;
+
+import junit.framework.TestCase;
+
+/**
+ * Implement this interface to receive a {@link Bundle} containing the command line arguments
+ * passed to the test runner into your JUnit3 test.
+ * <p/>
+ * The test runner will call {@link #injectBundle(Bundle)} after
+ * object construction but before any {@link TestCase#setUp()} methods are called.
+ * Note the order in which injectBundle is called vs other inject methods is not defined.
+ * <p/>
+ * Declaring this in a JUnit4 test will have no effect. Use {@link InjectBundle} instead.
+ */
+public interface BundleTest {
+
+ /**
+ * Called by Android test runner to pass in Bundle containing command line arguments.
+ */
+ public void injectBundle(Bundle bundle);
+}
diff --git a/androidtestlib/src/com/android/test/InjectBundle.java b/androidtestlib/src/com/android/test/InjectBundle.java
new file mode 100644
index 0000000..ee5e58e
--- /dev/null
+++ b/androidtestlib/src/com/android/test/InjectBundle.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test;
+
+import android.os.Bundle;
+
+import junit.framework.Test;
+
+import org.junit.Before;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this to inject a {@link Bundle} containing the command line arguments passed to the
+ * test runner into your JUnit4-style test.
+ * <p/>
+ * To use, just add the correct annotation to an {@link Bundle} field like this:
+ * <pre>
+ * @InjectBundle public Bundle mMyBundle;
+ * </pre>
+ * The test runner will set the value of this field with the {@link Bundle} after
+ * object construction but before any {@link Before} methods are called.
+ * <p/>
+ * Declaring this in a JUnit3 test (ie a class that is a {@link Test}) will have no effect.
+ * Use {@link BundleTest} instead.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface InjectBundle {
+
+}
diff --git a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
index a932523..6d63ef0 100644
--- a/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
+++ b/androidtestlib/src/com/android/test/runner/AndroidJUnitRunner.java
@@ -188,6 +188,8 @@
Debug.waitForDebugger();
}
+ setupDexmaker();
+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
PrintStream writer = new PrintStream(byteArrayOutputStream);
List<RunListener> listeners = new ArrayList<RunListener>();
@@ -305,7 +307,7 @@
if (getBooleanArgument(ARGUMENT_LOG_ONLY)) {
builder.setSkipExecution(true);
}
- return builder.build(this);
+ return builder.build(this, arguments);
}
/**
@@ -335,4 +337,11 @@
testRequestBuilder.addTestClass(testClassName);
}
}
+
+ private void setupDexmaker() {
+ // Explicitly set the Dexmaker cache, so tests that use mocking frameworks work
+ String dexCache = getTargetContext().getCacheDir().getPath();
+ Log.i(LOG_TAG, "Setting dexmaker.dexcache to " + dexCache);
+ System.setProperty("dexmaker.dexcache", getTargetContext().getCacheDir().getPath());
+ }
}
diff --git a/androidtestlib/src/com/android/test/runner/AndroidRunnerBuilder.java b/androidtestlib/src/com/android/test/runner/AndroidRunnerBuilder.java
index d56861a..e8c52a6 100644
--- a/androidtestlib/src/com/android/test/runner/AndroidRunnerBuilder.java
+++ b/androidtestlib/src/com/android/test/runner/AndroidRunnerBuilder.java
@@ -16,6 +16,7 @@
package com.android.test.runner;
import android.app.Instrumentation;
+import android.os.Bundle;
import com.android.test.runner.junit3.AndroidJUnit3Builder;
import com.android.test.runner.junit4.AndroidJUnit4Builder;
@@ -29,16 +30,14 @@
*/
class AndroidRunnerBuilder extends AllDefaultPossibilitiesBuilder {
- private final Instrumentation mInstr;
private final AndroidJUnit3Builder mAndroidJUnit3Builder;
private final AndroidJUnit4Builder mAndroidJUnit4Builder;
- public AndroidRunnerBuilder(boolean canUseSuiteMethod, Instrumentation instr,
+ public AndroidRunnerBuilder(boolean canUseSuiteMethod, Instrumentation instr, Bundle bundle,
boolean skipExecution) {
super(canUseSuiteMethod);
- mInstr = instr;
- mAndroidJUnit3Builder = new AndroidJUnit3Builder(mInstr, skipExecution);
- mAndroidJUnit4Builder = new AndroidJUnit4Builder(mInstr, skipExecution);
+ mAndroidJUnit3Builder = new AndroidJUnit3Builder(instr, bundle, skipExecution);
+ mAndroidJUnit4Builder = new AndroidJUnit4Builder(instr, bundle, skipExecution);
}
@Override
diff --git a/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java b/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
index 65a3fef..be83014 100644
--- a/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
+++ b/androidtestlib/src/com/android/test/runner/TestRequestBuilder.java
@@ -16,6 +16,7 @@
package com.android.test.runner;
import android.app.Instrumentation;
+import android.os.Bundle;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
@@ -250,13 +251,13 @@
* If no classes have been explicitly added, will scan the classpath for all tests.
*
*/
- public TestRequest build(Instrumentation instr) {
+ public TestRequest build(Instrumentation instr, Bundle bundle) {
if (mTestLoader.isEmpty()) {
// no class restrictions have been specified. Load all classes
loadClassesFromClassPath();
}
- Request request = classes(instr, mSkipExecution, new Computer(),
+ Request request = classes(instr, bundle, mSkipExecution, new Computer(),
mTestLoader.getLoadedClasses().toArray(new Class[0]));
return new TestRequest(mTestLoader.getLoadFailures(), request.filterWith(mFilter));
}
@@ -266,14 +267,17 @@
* in a set of classes.
*
* @param instr the {@link Instrumentation} to inject into any tests that require it
+ * @param bundle the {@link Bundle} of command line args to inject into any tests that require
+ * it
* @param computer Helps construct Runners from classes
* @param classes the classes containing the tests
* @return a <code>Request</code> that will cause all tests in the classes to be run
*/
- private static Request classes(Instrumentation instr, boolean skipExecution,
+ private static Request classes(Instrumentation instr, Bundle bundle, boolean skipExecution,
Computer computer, Class<?>... classes) {
try {
- AndroidRunnerBuilder builder = new AndroidRunnerBuilder(true, instr, skipExecution);
+ AndroidRunnerBuilder builder = new AndroidRunnerBuilder(true, instr, bundle,
+ skipExecution);
Runner suite = computer.getSuite(builder, classes);
return Request.runner(suite);
} catch (InitializationError e) {
@@ -299,6 +303,9 @@
new ExcludePackageNameFilter("junit"),
new ExcludePackageNameFilter("org.junit"),
new ExcludePackageNameFilter("org.hamcrest"),
+ new ExcludePackageNameFilter("org.mockito"),
+ new ExcludePackageNameFilter("com.android.dx"),
+ new ExcludePackageNameFilter("com.google.dexmaker"),
new ExternalClassNameFilter(),
new ExcludePackageNameFilter("com.android.test.runner.junit3")));
} catch (IOException e) {
diff --git a/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3Builder.java b/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3Builder.java
index c6b4408..e20f28a 100644
--- a/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3Builder.java
+++ b/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3Builder.java
@@ -16,6 +16,7 @@
package com.android.test.runner.junit3;
import android.app.Instrumentation;
+import android.os.Bundle;
import junit.framework.TestCase;
@@ -30,9 +31,11 @@
private Instrumentation mInstr;
private boolean mSkipExecution;
+ private final Bundle mBundle;
- public AndroidJUnit3Builder(Instrumentation instr, boolean skipExecution) {
+ public AndroidJUnit3Builder(Instrumentation instr, Bundle bundle, boolean skipExecution) {
mInstr = instr;
+ mBundle = bundle;
mSkipExecution = skipExecution;
}
@@ -41,9 +44,11 @@
if (mSkipExecution && isJUnit3TestCase(testClass)) {
return new NonExecutingJUnit3ClassRunner(testClass);
} else if (isAndroidTestCase(testClass)) {
- return new AndroidJUnit3ClassRunner(testClass, mInstr);
+ return new AndroidJUnit3ClassRunner(testClass, mBundle, mInstr);
} else if (isInstrumentationTestCase(testClass)) {
- return new AndroidJUnit3ClassRunner(testClass, mInstr);
+ return new AndroidJUnit3ClassRunner(testClass, mBundle, mInstr);
+ } else if (isBundleTest(testClass)) {
+ return new AndroidJUnit3ClassRunner(testClass, mBundle, mInstr);
}
return null;
}
@@ -59,4 +64,9 @@
boolean isInstrumentationTestCase(Class<?> testClass) {
return android.test.InstrumentationTestCase.class.isAssignableFrom(testClass);
}
+
+ boolean isBundleTest(Class<?> testClass) {
+ return com.android.test.BundleTest.class.isAssignableFrom(testClass);
+ }
+
}
diff --git a/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3ClassRunner.java b/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3ClassRunner.java
index 91ddc62..418a80b 100644
--- a/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3ClassRunner.java
+++ b/androidtestlib/src/com/android/test/runner/junit3/AndroidJUnit3ClassRunner.java
@@ -16,6 +16,7 @@
package com.android.test.runner.junit3;
import android.app.Instrumentation;
+import android.os.Bundle;
import junit.framework.TestCase;
import junit.framework.TestSuite;
@@ -27,18 +28,15 @@
*/
class AndroidJUnit3ClassRunner extends JUnit38ClassRunner {
- /**
- * @param klass
- */
- public AndroidJUnit3ClassRunner(Class<?> klass, Instrumentation instr) {
- super(new AndroidTestSuite(klass.asSubclass(TestCase.class), instr));
+ public AndroidJUnit3ClassRunner(Class<?> klass, Bundle bundle, Instrumentation instr) {
+ super(new AndroidTestSuite(klass.asSubclass(TestCase.class), bundle, instr));
}
@Override
protected TestSuite createCopyOfSuite(TestSuite s) {
if (s instanceof AndroidTestSuite) {
AndroidTestSuite a = (AndroidTestSuite)s;
- return new AndroidTestSuite(a.getName(), a.getInstrumentation());
+ return new AndroidTestSuite(a.getName(), a.getBundle(), a.getInstrumentation());
} else {
return super.createCopyOfSuite(s);
}
diff --git a/androidtestlib/src/com/android/test/runner/junit3/AndroidTestSuite.java b/androidtestlib/src/com/android/test/runner/junit3/AndroidTestSuite.java
index 01d8103..e1a90c3 100644
--- a/androidtestlib/src/com/android/test/runner/junit3/AndroidTestSuite.java
+++ b/androidtestlib/src/com/android/test/runner/junit3/AndroidTestSuite.java
@@ -15,17 +15,20 @@
*/
package com.android.test.runner.junit3;
-import android.app.Instrumentation;
-import android.content.Context;
-import android.test.AndroidTestCase;
-import android.test.InstrumentationTestCase;
-
import junit.framework.Test;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import org.junit.Ignore;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+import android.test.AndroidTestCase;
+import android.test.InstrumentationTestCase;
+
+import com.android.test.BundleTest;
+
/**
* A {@link TestSuite} used to pass {@link Context} and {@link Instrumentation} references to child
* tests.
@@ -34,14 +37,17 @@
class AndroidTestSuite extends TestSuite {
private final Instrumentation mInstr;
+ private final Bundle mBundle;
- AndroidTestSuite(Class<?> clazz, Instrumentation instrumentation) {
+ AndroidTestSuite(Class<?> clazz, Bundle bundle, Instrumentation instrumentation) {
super(clazz);
+ mBundle = bundle;
mInstr = instrumentation;
}
- AndroidTestSuite(String name, Instrumentation instrumentation) {
+ AndroidTestSuite(String name, Bundle bundle, Instrumentation instrumentation) {
super(name);
+ mBundle = bundle;
mInstr = instrumentation;
}
@@ -53,10 +59,17 @@
if (test instanceof InstrumentationTestCase) {
((InstrumentationTestCase)test).injectInstrumentation(mInstr);
}
+ if (test instanceof BundleTest) {
+ ((BundleTest)test).injectBundle(mBundle);
+ }
super.runTest(test, result);
}
Instrumentation getInstrumentation() {
return mInstr;
}
+
+ Bundle getBundle() {
+ return mBundle;
+ }
}
diff --git a/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4Builder.java b/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4Builder.java
index 1b14a9a..6a08b6a 100644
--- a/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4Builder.java
+++ b/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4Builder.java
@@ -16,6 +16,7 @@
package com.android.test.runner.junit4;
import android.app.Instrumentation;
+import android.os.Bundle;
import com.android.test.InjectContext;
import com.android.test.InjectInstrumentation;
@@ -32,10 +33,12 @@
public class AndroidJUnit4Builder extends RunnerBuilder {
private final Instrumentation mInstrumentation;
+ private final Bundle mBundle;
private boolean mSkipExecution;
- public AndroidJUnit4Builder(Instrumentation instr, boolean skipExecution) {
+ public AndroidJUnit4Builder(Instrumentation instr, Bundle bundle, boolean skipExecution) {
mInstrumentation = instr;
+ mBundle = bundle;
mSkipExecution = skipExecution;
}
@@ -45,7 +48,7 @@
return new NonExecutingJUnit4ClassRunner(testClass);
}
if (hasInjectedFields(testClass)) {
- return new AndroidJUnit4ClassRunner(testClass, mInstrumentation);
+ return new AndroidJUnit4ClassRunner(testClass, mInstrumentation, mBundle);
}
return null;
}
diff --git a/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4ClassRunner.java b/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4ClassRunner.java
index 41d5ad4..92bac8a 100644
--- a/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4ClassRunner.java
+++ b/androidtestlib/src/com/android/test/runner/junit4/AndroidJUnit4ClassRunner.java
@@ -17,8 +17,10 @@
import android.app.Instrumentation;
import android.content.Context;
+import android.os.Bundle;
import android.util.Log;
+import com.android.test.InjectBundle;
import com.android.test.InjectContext;
import com.android.test.InjectInstrumentation;
@@ -37,6 +39,7 @@
private static final String LOG_TAG = "AndroidJUnit4ClassRunner";
private final Instrumentation mInstr;
+ private final Bundle mBundle;
@SuppressWarnings("serial")
private static class InvalidInjectException extends Exception {
@@ -45,10 +48,11 @@
}
}
- public AndroidJUnit4ClassRunner(Class<?> klass, Instrumentation instr)
+ public AndroidJUnit4ClassRunner(Class<?> klass, Instrumentation instr, Bundle bundle)
throws InitializationError {
super(klass);
mInstr = instr;
+ mBundle = bundle;
}
@Override
@@ -76,6 +80,11 @@
for (FrameworkField contextField : contextFields) {
validateInjectField(errors, contextField, Context.class);
}
+ List<FrameworkField> bundleFields = getTestClass().getAnnotatedFields(
+ InjectBundle.class);
+ for (FrameworkField bundleField : bundleFields) {
+ validateInjectField(errors, bundleField, Bundle.class);
+ }
}
private void validateInjectField(List<Throwable> errors, FrameworkField instrField,
@@ -104,6 +113,11 @@
for (FrameworkField contextField : contextFields) {
setFieldValue(test, contextField.getField(), mInstr.getTargetContext());
}
+ List<FrameworkField> bundleFields = getTestClass().getAnnotatedFields(
+ InjectBundle.class);
+ for (FrameworkField bundleField : bundleFields) {
+ setFieldValue(test, bundleField.getField(), mBundle);
+ }
}
private void setFieldValue(Object test, Field field, Object value) {
diff --git a/androidtestlib/tests/.classpath b/androidtestlib/tests/.classpath
new file mode 100644
index 0000000..99b9c55
--- /dev/null
+++ b/androidtestlib/tests/.classpath
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="src" path="dexmaker-java-src"/>
+ <classpathentry kind="src" path="dexmaker-dx-src"/>
+ <classpathentry kind="src" path="hamcrest-src"/>
+ <classpathentry kind="src" path="junit4-src"/>
+ <classpathentry kind="src" path="android-test-lib-src"/>
+ <classpathentry kind="src" path="objenesis-src"/>
+ <classpathentry kind="src" path="mockito-src"/>
+ <classpathentry kind="src" path="dexmaker-mockito"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/androidtestlib/tests/.gitignore b/androidtestlib/tests/.gitignore
index 4f50fa0..4f62b84 100644
--- a/androidtestlib/tests/.gitignore
+++ b/androidtestlib/tests/.gitignore
@@ -1,2 +1 @@
-.classpath
gen
diff --git a/androidtestlib/tests/.project b/androidtestlib/tests/.project
new file mode 100644
index 0000000..e4e4c5e
--- /dev/null
+++ b/androidtestlib/tests/.project
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>AndroidTestLibTests</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+ <linkedResources>
+ <link>
+ <name>android-test-lib-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/frameworks/testing/androidtestlib/src</locationURI>
+ </link>
+ <link>
+ <name>dexmaker-dx-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/dexmaker/src/dx/java</locationURI>
+ </link>
+ <link>
+ <name>dexmaker-java-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/dexmaker/src/main/java</locationURI>
+ </link>
+ <link>
+ <name>dexmaker-mockito</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/dexmaker/src/mockito/java</locationURI>
+ </link>
+ <link>
+ <name>hamcrest-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/hamcrest/src</locationURI>
+ </link>
+ <link>
+ <name>junit4-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/junit/src</locationURI>
+ </link>
+ <link>
+ <name>littlemock-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/littlemock/src</locationURI>
+ </link>
+ <link>
+ <name>mockito-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/mockito/src</locationURI>
+ </link>
+ <link>
+ <name>objenesis-src</name>
+ <type>2</type>
+ <locationURI>ANDROID_TOP/external/objenesis/main/src</locationURI>
+ </link>
+ </linkedResources>
+</projectDescription>
diff --git a/androidtestlib/tests/Android.mk b/androidtestlib/tests/Android.mk
index 3d27857..fa27121 100644
--- a/androidtestlib/tests/Android.mk
+++ b/androidtestlib/tests/Android.mk
@@ -25,9 +25,10 @@
LOCAL_MODULE_TAGS := tests
-LOCAL_SDK_VERSION := 8
+# SDK 10 needed for mockito/objnesis. Otherwise 8 would work
+LOCAL_SDK_VERSION := 10
-LOCAL_STATIC_JAVA_LIBRARIES := android-test-lib dexmaker littlemock
+LOCAL_STATIC_JAVA_LIBRARIES := android-test-lib mockito-target dexmaker
LOCAL_PROGUARD_ENABLED := disabled
diff --git a/androidtestlib/tests/src/com/android/test/MyBundleTestCase.java b/androidtestlib/tests/src/com/android/test/MyBundleTestCase.java
new file mode 100644
index 0000000..7ed0fd4
--- /dev/null
+++ b/androidtestlib/tests/src/com/android/test/MyBundleTestCase.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.test;
+
+import junit.framework.TestCase;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Placeholder test to verify {@link Bundle} gets injected to {@link BundleTest}.
+ */
+public class MyBundleTestCase extends TestCase implements BundleTest {
+
+ private Bundle mBundle = null;
+
+ public MyBundleTestCase() {
+ Log.i("MyBundleTestCase", "I'm created");
+ }
+
+ @Override
+ public void injectBundle(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ public void testInjectBundleCalled() {
+ assertNotNull(mBundle);
+ }
+}
diff --git a/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java b/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
index 6a24a47..441c4f0 100644
--- a/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
+++ b/androidtestlib/tests/src/com/android/test/runner/AndroidJUnitRunnerTest.java
@@ -18,11 +18,12 @@
import android.content.Context;
import android.os.Bundle;
-import com.google.testing.littlemock.LittleMock;
-import com.google.testing.littlemock.Mock;
-
import org.junit.Before;
import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
@@ -57,7 +58,7 @@
};
mAndroidJUnitRunner.setArguments(new Bundle());
mStubStream = new PrintStream(new ByteArrayOutputStream());
- LittleMock.initMocks(this);
+ MockitoAnnotations.initMocks(this);
}
/**
@@ -68,9 +69,8 @@
public void testBuildRequest_singleClass() {
Bundle b = new Bundle();
b.putString(AndroidJUnitRunner.ARGUMENT_TEST_CLASS, "ClassName");
- LittleMock.doNothing().when(mMockBuilder).addTestClass("ClassName");
mAndroidJUnitRunner.buildRequest(b, mStubStream);
- LittleMock.verify(mMockBuilder);
+ Mockito.verify(mMockBuilder).addTestClass("ClassName");
}
/**
@@ -81,10 +81,9 @@
public void testBuildRequest_multiClass() {
Bundle b = new Bundle();
b.putString(AndroidJUnitRunner.ARGUMENT_TEST_CLASS, "ClassName1,ClassName2");
- LittleMock.doNothing().when(mMockBuilder).addTestClass("ClassName1");
- LittleMock.doNothing().when(mMockBuilder).addTestClass("ClassName2");
mAndroidJUnitRunner.buildRequest(b, mStubStream);
- LittleMock.verify(mMockBuilder);
+ Mockito.verify(mMockBuilder).addTestClass("ClassName1");
+ Mockito.verify(mMockBuilder).addTestClass("ClassName2");
}
/**
@@ -95,8 +94,7 @@
public void testBuildRequest_method() {
Bundle b = new Bundle();
b.putString(AndroidJUnitRunner.ARGUMENT_TEST_CLASS, "ClassName1#method");
- LittleMock.doNothing().when(mMockBuilder).addTestMethod("ClassName1", "method");
mAndroidJUnitRunner.buildRequest(b, mStubStream);
- LittleMock.verify(mMockBuilder);
+ Mockito.verify(mMockBuilder).addTestMethod("ClassName1", "method");
}
}
diff --git a/androidtestlib/tests/src/com/android/test/runner/TestRequestBuilderTest.java b/androidtestlib/tests/src/com/android/test/runner/TestRequestBuilderTest.java
index 4d7d9bb..793035e 100644
--- a/androidtestlib/tests/src/com/android/test/runner/TestRequestBuilderTest.java
+++ b/androidtestlib/tests/src/com/android/test/runner/TestRequestBuilderTest.java
@@ -16,8 +16,10 @@
package com.android.test.runner;
import android.app.Instrumentation;
+import android.os.Bundle;
import android.test.suitebuilder.annotation.SmallTest;
+import com.android.test.InjectBundle;
import com.android.test.InjectInstrumentation;
import org.junit.Assert;
@@ -60,6 +62,9 @@
@InjectInstrumentation
public Instrumentation mInstr;
+ @InjectBundle
+ public Bundle mBundle;
+
/**
* Test initial condition for size filtering - that all tests run when no filter is attached
*/
@@ -67,7 +72,7 @@
public void testNoSize() {
TestRequestBuilder b = new TestRequestBuilder(new PrintStream(new ByteArrayOutputStream()));
b.addTestClass(SampleTest.class.getName());
- TestRequest request = b.build(mInstr);
+ TestRequest request = b.build(mInstr, mBundle);
JUnitCore testRunner = new JUnitCore();
Result result = testRunner.run(request.getRequest());
Assert.assertEquals(2, result.getRunCount());
@@ -81,7 +86,7 @@
TestRequestBuilder b = new TestRequestBuilder(new PrintStream(new ByteArrayOutputStream()));
b.addTestClass(SampleTest.class.getName());
b.addTestSizeFilter("small");
- TestRequest request = b.build(mInstr);
+ TestRequest request = b.build(mInstr, mBundle);
JUnitCore testRunner = new JUnitCore();
Result result = testRunner.run(request.getRequest());
Assert.assertEquals(1, result.getRunCount());
@@ -96,7 +101,7 @@
b.addTestClass(SampleTest.class.getName());
b.addTestClass(SampleClassSize.class.getName());
b.addTestSizeFilter("small");
- TestRequest request = b.build(mInstr);
+ TestRequest request = b.build(mInstr, mBundle);
JUnitCore testRunner = new JUnitCore();
Result result = testRunner.run(request.getRequest());
Assert.assertEquals(3, result.getRunCount());
@@ -111,7 +116,7 @@
b.addAnnotationInclusionFilter(SmallTest.class.getName());
b.addTestClass(SampleTest.class.getName());
b.addTestClass(SampleClassSize.class.getName());
- TestRequest request = b.build(mInstr);
+ TestRequest request = b.build(mInstr, mBundle);
JUnitCore testRunner = new JUnitCore();
Result result = testRunner.run(request.getRequest());
Assert.assertEquals(3, result.getRunCount());
@@ -126,7 +131,7 @@
b.addAnnotationExclusionFilter(SmallTest.class.getName());
b.addTestClass(SampleTest.class.getName());
b.addTestClass(SampleClassSize.class.getName());
- TestRequest request = b.build(mInstr);
+ TestRequest request = b.build(mInstr, mBundle);
JUnitCore testRunner = new JUnitCore();
Result result = testRunner.run(request.getRequest());
Assert.assertEquals(1, result.getRunCount());
diff --git a/app-tests/Android.mk b/app-tests/Android.mk
new file mode 100644
index 0000000..c141484
--- /dev/null
+++ b/app-tests/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/app-tests/AppLaunchTest/Android.mk b/app-tests/AppLaunchTest/Android.mk
new file mode 100644
index 0000000..5e874d0
--- /dev/null
+++ b/app-tests/AppLaunchTest/Android.mk
@@ -0,0 +1,31 @@
+# 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_PACKAGE_NAME := AppLaunchTest
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := 8
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
+
diff --git a/app-tests/AppLaunchTest/AndroidManifest.xml b/app-tests/AppLaunchTest/AndroidManifest.xml
new file mode 100644
index 0000000..ef5f1ea
--- /dev/null
+++ b/app-tests/AppLaunchTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.applaunchtest" >
+
+ <instrumentation
+ android:name="com.android.applaunchtest.AppLaunchRunner"
+ android:targetPackage="com.android.applaunchtest" />
+
+ <application
+ android:label="@string/app_name" android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+</manifest>
diff --git a/app-tests/AppLaunchTest/res/values/strings.xml b/app-tests/AppLaunchTest/res/values/strings.xml
new file mode 100644
index 0000000..f8face5
--- /dev/null
+++ b/app-tests/AppLaunchTest/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<resources>
+
+ <string name="app_name">AppLaunchTest</string>
+
+</resources>
diff --git a/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchRunner.java b/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchRunner.java
new file mode 100644
index 0000000..08d0894
--- /dev/null
+++ b/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchRunner.java
@@ -0,0 +1,57 @@
+/*
+ * 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.applaunchtest;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestRunner;
+
+/**
+ * A special test runner that accepts arguments needed to run {@link AppLaunchTest}.
+ */
+public class AppLaunchRunner extends InstrumentationTestRunner {
+
+ private String mAppPackageName;
+ private long mWaitTime;
+
+ @Override
+ public void onCreate(Bundle args) {
+ mAppPackageName = args.getString("packageName");
+ String waitTimeString = args.getString("appLaunchWait");
+ if (waitTimeString != null) {
+ mWaitTime = Long.parseLong(waitTimeString);
+ } else {
+ // default to 7 seconds
+ mWaitTime = 7000;
+ }
+ super.onCreate(args);
+ }
+
+ /**
+ * Gets the Android application package name to launch. Application must have a launchable
+ * activity that handles MAIN intent.
+ */
+ public String getAppPackageName() {
+ return mAppPackageName;
+ }
+
+ /**
+ * Gets the number of ms to monitor for app crashes after attempting to launch the app.
+ */
+ public long getAppWaitTime() {
+ return mWaitTime;
+ }
+}
diff --git a/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchTest.java b/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchTest.java
new file mode 100644
index 0000000..257babc
--- /dev/null
+++ b/app-tests/AppLaunchTest/src/com/android/applaunchtest/AppLaunchTest.java
@@ -0,0 +1,319 @@
+/*
+ * 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.applaunchtest;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.ProcessErrorStateInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Simple tests that launches a specified app, and waits for a configurable amount of time for
+ * crashes and ANRs.
+ * <p/>
+ * If no crashes occur, test is considered passed.
+ * <p/>
+ * Derived from frameworks/base/tests/SmokeTests/... . TODO: consider refactoring to share code
+ */
+public class AppLaunchTest extends InstrumentationTestCase {
+
+ private static final String TAG = "AppLaunchTest";
+
+ private ActivityManager mActivityManager;
+ private PackageManager mPackageManager;
+ private String mPackageName;
+ private Context mContext;
+ private long mWaitTime;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = getInstrumentation().getTargetContext();
+ assertNotNull("failed to get context", mContext);
+
+ mActivityManager = (ActivityManager)
+ mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mPackageManager = mContext.getPackageManager();
+ assertNotNull("failed to get activity manager", mActivityManager);
+ assertNotNull("failed to get package manager", mPackageManager);
+
+ assertTrue("Unexpected runner: AppLaunchRunner must be used",
+ getInstrumentation() instanceof AppLaunchRunner);
+ AppLaunchRunner runner = (AppLaunchRunner)getInstrumentation();
+ mPackageName = runner.getAppPackageName();
+ mWaitTime = runner.getAppWaitTime();
+ assertNotNull("package name to launch was not provided", mPackageName);
+ assertNotNull("time to wait for app launch was not provided", mWaitTime);
+ }
+
+ /**
+ * A test that runs Launcher-launchable activity for given package name and verifies that no
+ * ANRs or crashes happened while doing so.
+ */
+ public void testLaunchActivity() throws Exception {
+ final Set<ProcessError> errSet = new LinkedHashSet<ProcessError>();
+
+ ResolveInfo app = getLauncherActivity(mPackageName, mPackageManager);
+ assertNotNull(String.format("Could not find launchable activity for %s", mPackageName),
+ app);
+ final Collection<ProcessError> errProcs = runOneActivity(app, mWaitTime);
+ if (errProcs != null) {
+ errSet.addAll(errProcs);
+ }
+
+ if (!errSet.isEmpty()) {
+ fail(String.format("Detected %d errors on launch of app %s:\n%s", errSet.size(),
+ mPackageName, reportWrappedListContents(errSet)));
+ }
+ }
+
+ /**
+ * A method to run the specified Activity and return a {@link Collection} of the Activities that
+ * were in an error state, as listed by {@link ActivityManager.getProcessesInErrorState()}.
+ * <p />
+ * The method will launch the app, wait for waitTime seconds, check for apps in the error state
+ * and then return.
+ */
+ public Collection<ProcessError> runOneActivity(ResolveInfo app, long appLaunchWait) {
+
+ Log.i(TAG, String.format("Running activity %s/%s", app.activityInfo.packageName,
+ app.activityInfo.name));
+
+ // We check for any Crash or ANR dialogs that are already up, and we ignore them. This is
+ // so that we don't report crashes that were caused by prior apps.
+ final Collection<ProcessError> preErrProcs =
+ ProcessError.fromCollection(mActivityManager.getProcessesInErrorState());
+
+ // launch app, and waitfor it to start/settle
+ final Intent intent = intentForActivity(app);
+ mContext.startActivity(intent);
+ try {
+ Thread.sleep(appLaunchWait);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+
+ // TODO: inject event to see if app is responding. The smoke tests press 'Home', but
+ // we don't want to do that here because we want to take screenshot on app launch
+
+ // See if there are any errors. We wait until down here to give ANRs as much time as
+ // possible to occur.
+ final Collection<ProcessError> errProcs =
+ ProcessError.fromCollection(mActivityManager.getProcessesInErrorState());
+
+ // Distinguish the asynchronous crashes/ANRs from the synchronous ones by checking the
+ // crash package name against the package name for {@code app}
+ if (errProcs != null) {
+ Iterator<ProcessError> errIter = errProcs.iterator();
+ while (errIter.hasNext()) {
+ ProcessError err = errIter.next();
+ if (!packageMatches(app, err)) {
+ // crash in another package. Just log it for now
+ Log.w(TAG, String.format("Detected crash in %s when launching %s",
+ err.info.processName, app.activityInfo.packageName));
+ errIter.remove();
+ }
+ }
+ }
+ // Take the difference between the remaining current error processes and the ones that were
+ // present when we started. The result is guaranteed to be:
+ // 1) Errors that are pertinent to this app's package
+ // 2) Errors that are pertinent to this particular app invocation
+ if (errProcs != null && preErrProcs != null) {
+ errProcs.removeAll(preErrProcs);
+ }
+
+ return errProcs;
+ }
+
+ /**
+ * A helper function that checks whether the specified error could have been caused by the
+ * specified app.
+ *
+ * @param app The app to check against
+ * @param err The error that we're considering
+ */
+ private static boolean packageMatches(ResolveInfo app, ProcessError err) {
+ final String appPkg = app.activityInfo.packageName;
+ final String errPkg = err.info.processName;
+ Log.d(TAG, String.format("packageMatches(%s, %s)", appPkg, errPkg));
+ return appPkg.equals(errPkg);
+ }
+
+ /**
+ * A helper function to get the launchable activity for the given package name.
+ */
+ static ResolveInfo getLauncherActivity(String packageName, PackageManager pm) {
+ final Intent launchable = new Intent(Intent.ACTION_MAIN);
+ launchable.addCategory(Intent.CATEGORY_LAUNCHER);
+ launchable.setPackage(packageName);
+ return pm.resolveActivity(launchable, 0);
+ }
+
+ /**
+ * A helper function to create an {@link Intent} to run, given a {@link ResolveInfo} specifying
+ * an activity to be launched.
+ */
+ static Intent intentForActivity(ResolveInfo app) {
+ final ComponentName component = new ComponentName(app.activityInfo.packageName,
+ app.activityInfo.name);
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(component);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ return intent;
+ }
+
+ /**
+ * Report error reports for {@link ProcessErrorStateInfo} instances that are wrapped inside of
+ * {@link ProcessError} instances. Just unwraps and calls
+ * {@see reportListContents(Collection<ProcessErrorStateInfo>)}.
+ */
+ static String reportWrappedListContents(Collection<ProcessError> errList) {
+ List<ProcessErrorStateInfo> newList = new ArrayList<ProcessErrorStateInfo>(errList.size());
+ for (ProcessError err : errList) {
+ newList.add(err.info);
+ }
+ return reportListContents(newList);
+ }
+
+ /**
+ * This helper function will dump the actual error reports.
+ *
+ * @param errList The error report containing one or more error records.
+ * @return Returns a string containing all of the errors.
+ */
+ private static String reportListContents(Collection<ProcessErrorStateInfo> errList) {
+ if (errList == null) return null;
+
+ StringBuilder builder = new StringBuilder();
+
+ Iterator<ProcessErrorStateInfo> iter = errList.iterator();
+ while (iter.hasNext()) {
+ ProcessErrorStateInfo entry = iter.next();
+
+ String condition;
+ switch (entry.condition) {
+ case ActivityManager.ProcessErrorStateInfo.CRASHED:
+ condition = "a CRASH";
+ break;
+ case ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING:
+ condition = "an ANR";
+ break;
+ default:
+ condition = "an unknown error";
+ break;
+ }
+
+ builder.append(String.format("Process %s encountered %s (%s)", entry.processName,
+ condition, entry.shortMsg));
+ if (entry.condition == ActivityManager.ProcessErrorStateInfo.CRASHED) {
+ builder.append(String.format(" with stack trace:\n%s\n", entry.stackTrace));
+ }
+ builder.append("\n");
+ }
+ return builder.toString();
+ }
+
+ /**
+ * A {@link ProcessErrorStateInfo} wrapper class that hashes how we want (so that equivalent
+ * crashes are considered equal).
+ */
+ static class ProcessError {
+ public final ProcessErrorStateInfo info;
+
+ public ProcessError(ProcessErrorStateInfo newInfo) {
+ info = newInfo;
+ }
+
+ public static Collection<ProcessError> fromCollection(Collection<ProcessErrorStateInfo> in)
+ {
+ if (in == null) {
+ return null;
+ }
+
+ List<ProcessError> out = new ArrayList<ProcessError>(in.size());
+ for (ProcessErrorStateInfo info : in) {
+ out.add(new ProcessError(info));
+ }
+ return out;
+ }
+
+ private boolean strEquals(String a, String b) {
+ if ((a == null) && (b == null)) {
+ return true;
+ } else if ((a == null) || (b == null)) {
+ return false;
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null) return false;
+ if (!(other instanceof ProcessError)) return false;
+ ProcessError peOther = (ProcessError) other;
+
+ return (info.condition == peOther.info.condition)
+ && strEquals(info.longMsg, peOther.info.longMsg)
+ && (info.pid == peOther.info.pid)
+ && strEquals(info.processName, peOther.info.processName)
+ && strEquals(info.shortMsg, peOther.info.shortMsg)
+ && strEquals(info.stackTrace, peOther.info.stackTrace)
+ && strEquals(info.tag, peOther.info.tag)
+ && (info.uid == peOther.info.uid);
+ }
+
+ private int hash(Object obj) {
+ if (obj == null) {
+ return 13;
+ } else {
+ return obj.hashCode();
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int code = 17;
+ code += info.condition;
+ code *= hash(info.longMsg);
+ code += info.pid;
+ code *= hash(info.processName);
+ code *= hash(info.shortMsg);
+ code *= hash(info.stackTrace);
+ code *= hash(info.tag);
+ code += info.uid;
+ return code;
+ }
+ }
+}
diff --git a/app-tests/README b/app-tests/README
new file mode 100644
index 0000000..248e72d
--- /dev/null
+++ b/app-tests/README
@@ -0,0 +1,2 @@
+Contains generic Android target tests that can be run against any application.
+
diff --git a/uiautomator/api/current.txt b/uiautomator/api/current.txt
index a1d80c4..a805070 100644
--- a/uiautomator/api/current.txt
+++ b/uiautomator/api/current.txt
@@ -1,5 +1,19 @@
package com.android.uiautomator.core {
+ public final class Configurator {
+ method public long getActionAcknowledgmentTimeout();
+ method public static com.android.uiautomator.core.Configurator getInstance();
+ method public long getKeyInjectionDelay();
+ method public long getScrollAcknowledgmentTimeout();
+ method public long getWaitForIdleTimeout();
+ method public long getWaitForSelectorTimeout();
+ method public com.android.uiautomator.core.Configurator setActionAcknowledgmentTimeout(long);
+ method public com.android.uiautomator.core.Configurator setKeyInjectionDelay(long);
+ method public com.android.uiautomator.core.Configurator setScrollAcknowledgmentTimeout(long);
+ method public com.android.uiautomator.core.Configurator setWaitForIdleTimeout(long);
+ method public com.android.uiautomator.core.Configurator setWaitForSelectorTimeout(long);
+ }
+
public class UiCollection extends com.android.uiautomator.core.UiObject {
ctor public UiCollection(com.android.uiautomator.core.UiSelector);
method public com.android.uiautomator.core.UiObject getChildByDescription(com.android.uiautomator.core.UiSelector, java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException;
@@ -11,12 +25,14 @@
public class UiDevice {
method public void clearLastTraversedText();
method public boolean click(int, int);
+ method public boolean drag(int, int, int, int, int);
method public void dumpWindowHierarchy(java.lang.String);
method public void freezeRotation() throws android.os.RemoteException;
method public deprecated java.lang.String getCurrentActivityName();
method public java.lang.String getCurrentPackageName();
method public int getDisplayHeight();
method public int getDisplayRotation();
+ method public android.graphics.Point getDisplaySizeDp();
method public int getDisplayWidth();
method public static com.android.uiautomator.core.UiDevice getInstance();
method public java.lang.String getLastTraversedText();
@@ -66,11 +82,14 @@
method public boolean clickAndWaitForNewWindow(long) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean clickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean clickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public boolean dragTo(com.android.uiautomator.core.UiObject, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public boolean dragTo(int, int, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean exists();
method protected android.view.accessibility.AccessibilityNodeInfo findAccessibilityNodeInfo(long);
method public android.graphics.Rect getBounds() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public com.android.uiautomator.core.UiObject getChild(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public int getChildCount() throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public java.lang.String getClassName() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public java.lang.String getContentDescription() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public com.android.uiautomator.core.UiObject getFromParent(com.android.uiautomator.core.UiSelector) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public java.lang.String getPackageName() throws com.android.uiautomator.core.UiObjectNotFoundException;
@@ -89,17 +108,22 @@
method public boolean longClick() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean longClickBottomRight() throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean longClickTopLeft() throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public void multiPointerGesture(android.view.MotionEvent.PointerCoords...);
+ method public void pinchIn(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public void pinchOut(int, int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean setText(java.lang.String) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean swipeDown(int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean swipeLeft(int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean swipeRight(int) throws com.android.uiautomator.core.UiObjectNotFoundException;
method public boolean swipeUp(int) throws com.android.uiautomator.core.UiObjectNotFoundException;
+ method public void twoPointerGesture(android.graphics.Point, android.graphics.Point, android.graphics.Point, android.graphics.Point, int);
method public boolean waitForExists(long);
method public boolean waitUntilGone(long);
+ field protected static final int FINGER_TOUCH_HALF_WIDTH = 20; // 0x14
field protected static final int SWIPE_MARGIN_LIMIT = 5; // 0x5
- field protected static final long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L
+ field protected static final deprecated long WAIT_FOR_EVENT_TMEOUT = 3000L; // 0xbb8L
field protected static final long WAIT_FOR_SELECTOR_POLL = 1000L; // 0x3e8L
- field protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L
+ field protected static final deprecated long WAIT_FOR_SELECTOR_TIMEOUT = 10000L; // 0x2710L
field protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500L; // 0x157cL
}
@@ -140,6 +164,7 @@
public class UiSelector {
ctor public UiSelector();
+ method public com.android.uiautomator.core.UiSelector checkable(boolean);
method public com.android.uiautomator.core.UiSelector checked(boolean);
method public com.android.uiautomator.core.UiSelector childSelector(com.android.uiautomator.core.UiSelector);
method public com.android.uiautomator.core.UiSelector className(java.lang.String);
@@ -160,6 +185,7 @@
method public com.android.uiautomator.core.UiSelector longClickable(boolean);
method public com.android.uiautomator.core.UiSelector packageName(java.lang.String);
method public com.android.uiautomator.core.UiSelector packageNameMatches(java.lang.String);
+ method public com.android.uiautomator.core.UiSelector resourceId(java.lang.String);
method public com.android.uiautomator.core.UiSelector scrollable(boolean);
method public com.android.uiautomator.core.UiSelector selected(boolean);
method public com.android.uiautomator.core.UiSelector text(java.lang.String);
diff --git a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
index cceef88..13d7f36 100644
--- a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
+++ b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
@@ -16,14 +16,19 @@
package com.android.commands.uiautomator;
-import android.accessibilityservice.UiTestAutomationBridge;
+import android.app.UiAutomation;
+import android.graphics.Point;
+import android.hardware.display.DisplayManagerGlobal;
import android.os.Environment;
+import android.view.Display;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.commands.uiautomator.Launcher.Command;
import com.android.uiautomator.core.AccessibilityNodeInfoDumper;
+import com.android.uiautomator.core.UiAutomationShellWrapper;
import java.io.File;
+import java.util.concurrent.TimeoutException;
/**
* Implementation of the dump subcommand
@@ -57,21 +62,33 @@
if (args.length > 0) {
dumpFile = new File(args[0]);
}
- UiTestAutomationBridge bridge = new UiTestAutomationBridge();
- bridge.connect();
+ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
+ automationWrapper.connect();
// It appears that the bridge needs time to be ready. Making calls to the
// bridge immediately after connecting seems to cause exceptions. So let's also
// do a wait for idle in case the app is busy.
- bridge.waitForIdle(1000, 1000 * 10);
- AccessibilityNodeInfo info = bridge.getRootAccessibilityNodeInfoInActiveWindow();
- if (info == null) {
- System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
+ try {
+ UiAutomation uiAutomation = automationWrapper.getUiAutomation();
+ uiAutomation.waitForIdle(1000, 1000 * 10);
+ AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
+ if (info == null) {
+ System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
+ return;
+ }
+
+ Display display =
+ DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
+ int rotation = display.getRotation();
+ Point size = new Point();
+ display.getSize(size);
+ AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
+ } catch (TimeoutException re) {
+ System.err.println("ERROR: could not get idle state.");
return;
+ } finally {
+ automationWrapper.disconnect();
}
- AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile);
- bridge.disconnect();
System.out.println(
String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
}
-
}
diff --git a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java
index 79428e9..ce55f18 100644
--- a/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java
+++ b/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/EventsCommand.java
@@ -16,10 +16,11 @@
package com.android.commands.uiautomator;
-import android.accessibilityservice.UiTestAutomationBridge;
+import android.app.UiAutomation.OnAccessibilityEventListener;
import android.view.accessibility.AccessibilityEvent;
import com.android.commands.uiautomator.Launcher.Command;
+import com.android.uiautomator.core.UiAutomationShellWrapper;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -49,15 +50,17 @@
@Override
public void run(String[] args) {
- final UiTestAutomationBridge bridge = new UiTestAutomationBridge() {
+ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
+ automationWrapper.connect();
+ automationWrapper.getUiAutomation().setOnAccessibilityEventListener(
+ new OnAccessibilityEventListener() {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
SimpleDateFormat formatter = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
System.out.println(String.format("%s %s",
formatter.format(new Date()), event.toString()));
}
- };
- bridge.connect();
+ });
// there's really no way to stop, essentially we just block indefinitely here and wait
// for user to press Ctrl+C
synchronized (mQuitLock) {
@@ -67,5 +70,6 @@
e.printStackTrace();
}
}
+ automationWrapper.disconnect();
}
}
diff --git a/uiautomator/instrumentation/Android.mk b/uiautomator/instrumentation/Android.mk
new file mode 100644
index 0000000..ed20993
--- /dev/null
+++ b/uiautomator/instrumentation/Android.mk
@@ -0,0 +1,29 @@
+#
+# 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 := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, testrunner-src) \
+ $(call all-java-files-under, ../library/core-src)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_MODULE := uiautomator-instrumentation
+# below to be uncommented once core-src is using public API only
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java
new file mode 100644
index 0000000..1d4fb93
--- /dev/null
+++ b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/core/InstrumentationUiAutomatorBridge.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.core;
+
+import android.app.Service;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.PowerManager;
+import android.view.Display;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+
+/**
+ * @hide
+ */
+public class InstrumentationUiAutomatorBridge extends UiAutomatorBridge {
+
+ private final Context mContext;
+
+ public InstrumentationUiAutomatorBridge(Context context, UiAutomation uiAutomation) {
+ super(uiAutomation);
+ mContext = context;
+ }
+
+ public Display getDefaultDisplay() {
+ WindowManager windowManager = (WindowManager)
+ mContext.getSystemService(Service.WINDOW_SERVICE);
+ return windowManager.getDefaultDisplay();
+ }
+
+ @Override
+ public int getRotation() {
+ return getDefaultDisplay().getRotation();
+ }
+
+ @Override
+ public boolean isScreenOn() {
+ PowerManager pm = (PowerManager)
+ mContext.getSystemService(Service.POWER_SERVICE);
+ return pm.isScreenOn();
+ }
+
+ public long getSystemLongPressTime() {
+ return ViewConfiguration.getLongPressTimeout();
+ }
+}
diff --git a/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java
new file mode 100644
index 0000000..ae763f2
--- /dev/null
+++ b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorInstrumentationTestRunner.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.testrunner;
+
+import android.test.AndroidTestRunner;
+import android.test.InstrumentationTestRunner;
+
+import com.android.uiautomator.core.Tracer;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestListener;
+
+/**
+ * Test runner for {@link UiAutomatorTestCase}s. Such tests are executed
+ * on the device and have access to an applications context.
+ */
+public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner {
+
+ @Override
+ public void onStart() {
+ // process runner arguments before test starts
+ String traceType = getArguments().getString("traceOutputMode");
+ if(traceType != null) {
+ Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType);
+ if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) {
+ String filename = getArguments().getString("traceLogFilename");
+ if (filename == null) {
+ throw new RuntimeException("Name of log file not specified. " +
+ "Please specify it using traceLogFilename parameter");
+ }
+ Tracer.getInstance().setOutputFilename(filename);
+ }
+ Tracer.getInstance().setOutputMode(mode);
+ }
+ super.onStart();
+ }
+
+ @Override
+ protected AndroidTestRunner getAndroidTestRunner() {
+ AndroidTestRunner testRunner = super.getAndroidTestRunner();
+ testRunner.addTestListener(new TestListener() {
+ @Override
+ public void startTest(Test test) {
+ if (test instanceof UiAutomatorTestCase) {
+ ((UiAutomatorTestCase)test).initialize(getArguments());
+ }
+ }
+
+ @Override
+ public void endTest(Test test) {
+ }
+
+ @Override
+ public void addFailure(Test test, AssertionFailedError e) {
+ }
+
+ @Override
+ public void addError(Test test, Throwable t) {
+ }
+ });
+ return testRunner;
+ }
+}
diff --git a/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
new file mode 100644
index 0000000..eaaef14
--- /dev/null
+++ b/uiautomator/instrumentation/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.testrunner;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.test.InstrumentationTestCase;
+
+import com.android.uiautomator.core.InstrumentationUiAutomatorBridge;
+import com.android.uiautomator.core.UiDevice;
+
+/**
+ * UI Automator test case that is executed on the device.
+ */
+public class UiAutomatorTestCase extends InstrumentationTestCase {
+
+ private Bundle mParams;
+
+ /**
+ * Get current instance of {@link UiDevice}. Works similar to calling the static
+ * {@link UiDevice#getInstance()} from anywhere in the test classes.
+ * @since API Level 16
+ */
+ public UiDevice getUiDevice() {
+ return UiDevice.getInstance();
+ }
+
+ /**
+ * 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.
+ * @since API Level 16
+ */
+ public Bundle getParams() {
+ return mParams;
+ }
+
+ /**
+ * Initializes this test case.
+ *
+ * @param params Instrumentation arguments.
+ */
+ void initialize(Bundle params) {
+ mParams = params;
+ UiDevice.getInstance().initialize(new InstrumentationUiAutomatorBridge(
+ getInstrumentation().getContext(),
+ getInstrumentation().getUiAutomation()));
+ }
+
+ /**
+ * Calls {@link SystemClock#sleep(long)} to sleep
+ * @param ms is in milliseconds.
+ * @since API Level 16
+ */
+ public void sleep(long ms) {
+ SystemClock.sleep(ms);
+ }
+}
diff --git a/uiautomator/library/Android.mk b/uiautomator/library/Android.mk
index d4b745a..cd0b5a1 100644
--- a/uiautomator/library/Android.mk
+++ b/uiautomator/library/Android.mk
@@ -16,7 +16,8 @@
LOCAL_PATH:= $(call my-dir)
-uiautomator.core_src_files := $(call all-java-files-under, src)
+uiautomator.core_src_files := $(call all-java-files-under, core-src) \
+ $(call all-java-files-under, testrunner-src)
uiautomator.core_java_libraries := android.test.runner core-junit
uiautomator_internal_api_file := $(TARGET_OUT_COMMON_INTERMEDIATES)/PACKAGING/uiautomator_api.txt
@@ -34,7 +35,8 @@
LOCAL_SRC_FILES := $(uiautomator.core_src_files)
LOCAL_JAVA_LIBRARIES := $(uiautomator.core_java_libraries)
LOCAL_MODULE_CLASS := JAVA_LIBRARIES
-LOCAL_DROIDDOC_SOURCE_PATH := $(LOCAL_PATH)/src
+LOCAL_DROIDDOC_SOURCE_PATH := $(LOCAL_PATH)/core-src \
+ $(LOCAL_PATH)/testrunner-src
LOCAL_DROIDDOC_HTML_DIR :=
LOCAL_DROIDDOC_OPTIONS:= \
diff --git a/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
similarity index 88%
rename from uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
index 10878e3..63c51e8 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
@@ -16,21 +16,19 @@
package com.android.uiautomator.core;
-import android.hardware.display.DisplayManagerGlobal;
import android.os.Environment;
import android.os.SystemClock;
import android.util.Log;
import android.util.Xml;
-import android.view.Display;
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;
-import org.xmlpull.v1.XmlSerializer;
-
/**
*
* @hide
@@ -46,9 +44,13 @@
/**
* Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
* and generates an xml dump into the /data/local/window_dump.xml
- * @param info
+ * @param root The root accessibility node.
+ * @param rotation The rotaion of current display
+ * @param width The pixel width of current display
+ * @param height The pixel height of current display
*/
- public static void dumpWindowToFile(AccessibilityNodeInfo info) {
+ public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
+ int width, int height) {
File baseDir = new File(Environment.getDataDirectory(), "local");
if (!baseDir.exists()) {
baseDir.mkdir();
@@ -56,8 +58,9 @@
baseDir.setWritable(true, false);
baseDir.setReadable(true, false);
}
- dumpWindowToFile(info, new File(
- new File(Environment.getDataDirectory(), "local"), "window_dump.xml"));
+ dumpWindowToFile(root,
+ new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
+ rotation, width, height);
}
/**
@@ -65,8 +68,12 @@
* and generates an xml dump to the location specified by <code>dumpFile</code>
* @param root The root accessibility node.
* @param dumpFile The file to dump to.
+ * @param rotation The rotaion of current display
+ * @param width The pixel width of current display
+ * @param height The pixel height of current display
*/
- public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile) {
+ public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
+ int width, int height) {
if (root == null) {
return;
}
@@ -78,10 +85,8 @@
serializer.setOutput(stringWriter);
serializer.startDocument("UTF-8", true);
serializer.startTag("", "hierarchy");
- serializer.attribute("", "rotation", Integer.toString(
- DisplayManagerGlobal.getInstance().getRealDisplay(
- Display.DEFAULT_DISPLAY).getRotation()));
- dumpNodeRec(root, serializer, 0);
+ serializer.attribute("", "rotation", Integer.toString(rotation));
+ dumpNodeRec(root, serializer, 0, width, height);
serializer.endTag("", "hierarchy");
serializer.endDocument();
writer.write(stringWriter.toString());
@@ -93,13 +98,14 @@
Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
}
- private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,
- int index) throws IOException {
+ private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
+ int width, int height) throws IOException {
serializer.startTag("", "node");
if (!nafExcludedClass(node) && !nafCheck(node))
serializer.attribute("", "NAF", Boolean.toString(true));
serializer.attribute("", "index", Integer.toString(index));
serializer.attribute("", "text", safeCharSeqToString(node.getText()));
+ serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
@@ -113,14 +119,14 @@
serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
serializer.attribute("", "password", Boolean.toString(node.isPassword()));
serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
- serializer.attribute("", "bounds",
- AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node).toShortString());
+ serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
+ node, width, height).toShortString());
int count = node.getChildCount();
for (int i = 0; i < count; i++) {
AccessibilityNodeInfo child = node.getChild(i);
if (child != null) {
if (child.isVisibleToUser()) {
- dumpNodeRec(child, serializer, i);
+ dumpNodeRec(child, serializer, i, width, height);
child.recycle();
} else {
Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
diff --git a/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
similarity index 79%
rename from uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
index d66e408..54835e3 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoHelper.java
@@ -15,10 +15,7 @@
*/
package com.android.uiautomator.core;
-import android.graphics.Point;
import android.graphics.Rect;
-import android.hardware.display.DisplayManagerGlobal;
-import android.view.Display;
import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -31,9 +28,11 @@
* Returns the node's bounds clipped to the size of the display
*
* @param node
+ * @param width pixel width of the display
+ * @param height pixel height of the display
* @return null if node is null, else a Rect containing visible bounds
*/
- static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node) {
+ static Rect getVisibleBoundsInScreen(AccessibilityNodeInfo node, int width, int height) {
if (node == null) {
return null;
}
@@ -42,14 +41,10 @@
node.getBoundsInScreen(nodeRect);
Rect displayRect = new Rect();
- Display display = DisplayManagerGlobal.getInstance()
- .getRealDisplay(Display.DEFAULT_DISPLAY);
- Point outSize = new Point();
- display.getSize(outSize);
displayRect.top = 0;
displayRect.left = 0;
- displayRect.right = outSize.x;
- displayRect.bottom = outSize.y;
+ displayRect.right = width;
+ displayRect.bottom = height;
nodeRect.intersect(displayRect);
return nodeRect;
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java b/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java
new file mode 100644
index 0000000..55ed3e7
--- /dev/null
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/Configurator.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.uiautomator.core;
+
+/**
+ * The Configurator allows for modification of some key framework parameters.
+ * New settings take effect immediately and can be changed any time during a test run.
+ *
+ * Can be obtained by calling {@link #getInstance()}
+ * @since API Level 18
+ */
+public final class Configurator {
+ private long mWaitForIdleTimeout = 10 * 1000;
+ private long mWaitForSelector = 10 * 1000;
+ private long mWaitForActionAcknowledgment = 3 * 1000;
+
+ // The events for a scroll typically complete even before touchUp occurs.
+ // This short timeout to make sure we get the very last in cases where the above isn't true.
+ private long mScrollEventWaitTimeout = 200; // ms
+
+ // Default is inject as fast as we can
+ private long mKeyInjectionDelay = 0; // ms
+
+ // reference to self
+ private static Configurator sConfigurator;
+
+ private Configurator() {
+ /* hide constructor */
+ }
+
+ /**
+ * Retrieves a singleton instance of Configurator
+ *
+ * @return Configurator instance
+ * @since API Level 18
+ */
+ public static Configurator getInstance() {
+ if (sConfigurator == null) {
+ sConfigurator = new Configurator();
+ }
+ return sConfigurator;
+ }
+
+ /**
+ * Sets the timeout used to wait for user interface idle before an automation action.
+ *
+ * All automation APIs, except {@link UiDevice}, perform this wait for idle before looking for
+ * the widget specified by the object's {@link UiSelector}. Once idle is detected, a
+ * wait for selector will begin. See {@link #setWaitForSelectorTimeout(long)}
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setWaitForIdleTimeout(long timeout) {
+ mWaitForIdleTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the timeout used to wait for user interface idle before an automation action.
+ *
+ * All automation APIs, except {@link UiDevice}, perform this wait for idle before looking for
+ * the widget specified by the object's {@link UiSelector}. Once idle is detected, a
+ * wait for selector will begin. See {@link #setWaitForSelectorTimeout(long)}
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getWaitForIdleTimeout() {
+ return mWaitForIdleTimeout;
+ }
+
+ /**
+ * Sets the timeout used to wait for a widget matching a selector to become visible
+ * in user interface.
+ *
+ * The user interface content is dynamic and sometimes the specific widget to be
+ * automated may not yet be visible. This wait allows the framework to keep looking
+ * for a matching widget to the object's {@link UiSelector}, up until the timeout.
+ *
+ * @param timeout Timeout value in milliseconds.
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setWaitForSelectorTimeout(long timeout) {
+ mWaitForSelector = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the timeout used to wait for a widget matching a selector to become visible
+ * in user interface.
+ *
+ * The user interface content is dynamic and sometimes the specific widget to be
+ * automated, may not yet be visible. This wait allows the framework to keep looking
+ * for a matching widget to the object's {@link UiSelector}, up until the timeout.
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getWaitForSelectorTimeout() {
+ return mWaitForSelector;
+ }
+
+ /**
+ * Sets the timeout used to wait for acknowledgment events caused by automation scroll
+ * swipe action.
+ *
+ * The acknowledgment event is an accessibility event, corresponding to the scroll action.
+ * This acknowledgment helps the framework determine if the requested action was successful.
+ * Changing this timeout value should only be made in very rare cases and in general use,
+ * should not be modified.
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setScrollAcknowledgmentTimeout(long timeout) {
+ mScrollEventWaitTimeout = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the timeout used to wait for acknowledgment events caused by automation scroll
+ * swipe action.
+ *
+ * The acknowledgment event is an accessibility event, corresponding to the scroll action.
+ * This acknowledgment helps the framework determine if the requested action was successful.
+ * Changing this timeout value should only be made in very rare cases and in general use,
+ * should not be modified.
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getScrollAcknowledgmentTimeout() {
+ return mScrollEventWaitTimeout;
+ }
+
+ /**
+ * Sets the timeout used to wait for acknowledgment events caused by generic automation
+ * actions such as clicks, setText, pressMenu etc...
+ *
+ * The acknowledgment event is an accessibility event, corresponding to an action, that the
+ * framework looks for after an action is performed. This acknowledgment helps the
+ * framework determine if the requested action was successful. Changing this timeout
+ * value should only be made in very rare cases and in general use, should not be modified.
+ *
+ * @param timeout Timeout value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setActionAcknowledgmentTimeout(long timeout) {
+ mWaitForActionAcknowledgment = timeout;
+ return this;
+ }
+
+ /**
+ * Gets the timeout used to wait for acknowledgment events caused by generic automation
+ * actions such as clicks, setText, pressMenu etc...
+ *
+ * The acknowledgment event is an accessibility event, corresponding to an action, that the
+ * framework looks for after an action is performed. This acknowledgment helps the
+ * framework determine if the requested action was successful. Changing this timeout
+ * value should only be made in very rare cases and in general use, should not be modified.
+ *
+ * @return current timeout in milliseconds
+ * @since API Level 18
+ */
+ public long getActionAcknowledgmentTimeout() {
+ return mWaitForActionAcknowledgment;
+ }
+
+ /**
+ * Sets a delay between key presses when sending text.
+ *
+ * @param delay Delay value in milliseconds
+ * @return self
+ * @since API Level 18
+ */
+ public Configurator setKeyInjectionDelay(long delay) {
+ mKeyInjectionDelay = delay;
+ return this;
+ }
+
+ /**
+ * Gets the delay between key presses when sending text.
+ *
+ * @return current delay in milliseconds
+ * @since API Level 18
+ */
+ public long getKeyInjectionDelay() {
+ return mKeyInjectionDelay;
+ }
+}
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
new file mode 100644
index 0000000..286895d
--- /dev/null
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uiautomator.core;
+
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.graphics.Point;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.util.Predicate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * The InteractionProvider is responsible for injecting user events such as touch events
+ * (includes swipes) and text key events into the system. To do so, all it needs to know about
+ * are coordinates of the touch events and text for the text input events.
+ * The InteractionController performs no synchronization. It will fire touch and text input events
+ * as fast as it receives them. All idle synchronization is performed prior to querying the
+ * hierarchy. See {@link QueryController}
+ */
+class InteractionController {
+
+ private static final String LOG_TAG = InteractionController.class.getSimpleName();
+
+ private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
+
+ private final KeyCharacterMap mKeyCharacterMap =
+ KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+
+ private final UiAutomatorBridge mUiAutomatorBridge;
+
+ private static final long REGULAR_CLICK_LENGTH = 100;
+
+ private long mDownTime;
+
+ // Inserted after each motion event injection.
+ private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
+
+ public InteractionController(UiAutomatorBridge bridge) {
+ mUiAutomatorBridge = bridge;
+ }
+
+ /**
+ * Predicate for waiting for any of the events specified in the mask
+ */
+ class WaitForAnyEventPredicate implements AccessibilityEventFilter {
+ int mMask;
+ WaitForAnyEventPredicate(int mask) {
+ mMask = mask;
+ }
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ return true;
+ }
+
+ // no match yet
+ return false;
+ }
+ }
+
+ /**
+ * Predicate for waiting for all the events specified in the mask and populating
+ * a ctor passed list with matching events. User of this Predicate must recycle
+ * all populated events in the events list.
+ */
+ class EventCollectingPredicate implements AccessibilityEventFilter {
+ int mMask;
+ List<AccessibilityEvent> mEventsList;
+
+ EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
+ mMask = mask;
+ mEventsList = events;
+ }
+
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ // For the events you need, always store a copy when returning false from
+ // predicates since the original will automatically be recycled after the call.
+ mEventsList.add(AccessibilityEvent.obtain(t));
+ }
+
+ // get more
+ return false;
+ }
+ }
+
+ /**
+ * Predicate for waiting for every event specified in the mask to be matched at least once
+ */
+ class WaitForAllEventPredicate implements AccessibilityEventFilter {
+ int mMask;
+ WaitForAllEventPredicate(int mask) {
+ mMask = mask;
+ }
+
+ @Override
+ public boolean accept(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ // remove from mask since this condition is satisfied
+ mMask &= ~t.getEventType();
+
+ // Since we're waiting for all events to be matched at least once
+ if (mMask != 0)
+ return false;
+
+ // all matched
+ return true;
+ }
+
+ // no match yet
+ return false;
+ }
+ }
+
+ /**
+ * Helper used by methods to perform actions and wait for any accessibility events and return
+ * predicated on predefined filter.
+ *
+ * @param command
+ * @param filter
+ * @param timeout
+ * @return
+ */
+ private AccessibilityEvent runAndWaitForEvents(Runnable command,
+ AccessibilityEventFilter filter, long timeout) {
+
+ try {
+ return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter,
+ timeout);
+ } catch (TimeoutException e) {
+ Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
+ return null;
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
+ return null;
+ }
+ }
+
+ /**
+ * Send keys and blocks until the first specified accessibility event.
+ *
+ * Most key presses will cause some UI change to occur. If the device is busy, this will
+ * block until the device begins to process the key press at which point the call returns
+ * and normal wait for idle processing may begin. If no events are detected for the
+ * timeout period specified, the call will return anyway with false.
+ *
+ * @param keyCode
+ * @param metaState
+ * @param eventType
+ * @param timeout
+ * @return true if events is received, otherwise false.
+ */
+ public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
+ final int eventType, long timeout) {
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ injectEventSync(upEvent);
+ }
+ }
+ };
+
+ return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
+ != null;
+ }
+
+ /**
+ * Clicks at coordinates without waiting for device idle. This may be used for operations
+ * that require stressing the target.
+ * @param x
+ * @param y
+ * @return true if the click executed successfully
+ */
+ public boolean clickNoSync(int x, int y) {
+ Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");
+
+ if (touchDown(x, y)) {
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ if (touchUp(x, y))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
+ * or TYPE_VIEW_SELECTED are received.
+ *
+ * @param x
+ * @param y
+ * @param timeout waiting for event
+ * @return true if events are received, else false if timeout.
+ */
+ public boolean clickAndSync(final int x, final int y, long timeout) {
+
+ String logString = String.format("clickAndSync(%d, %d)", x, y);
+ Log.d(LOG_TAG, logString);
+
+ return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate(
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
+ AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
+ }
+
+ /**
+ * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
+ * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
+ * no further waits will be performed and the function returns.
+ * @param x
+ * @param y
+ * @param timeout waiting for event
+ * @return true if both events occurred in the expected order
+ */
+ public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
+ String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y);
+ Log.d(LOG_TAG, logString);
+
+ return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate(
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null;
+ }
+
+ /**
+ * Returns a Runnable for use in {@link #runAndWaitForEvents(Runnable, Predicate, long) to
+ * perform a click.
+ *
+ * @param x coordinate
+ * @param y coordinate
+ * @return Runnable
+ */
+ private Runnable clickRunnable(final int x, final int y) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ if(touchDown(x, y)) {
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ touchUp(x, y);
+ }
+ }
+ };
+ }
+
+ /**
+ * Touches down for a long press at the specified coordinates.
+ *
+ * @param x
+ * @param y
+ * @return true if successful.
+ */
+ public boolean longTapNoSync(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
+ }
+
+ if (touchDown(x, y)) {
+ SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
+ if(touchUp(x, y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean touchDown(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
+ }
+ mDownTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ return injectEventSync(event);
+ }
+
+ private boolean touchUp(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
+ }
+ final long eventTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ mDownTime = 0;
+ return injectEventSync(event);
+ }
+
+ private boolean touchMove(int x, int y) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
+ }
+ final long eventTime = SystemClock.uptimeMillis();
+ MotionEvent event = MotionEvent.obtain(
+ mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
+ event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+ return injectEventSync(event);
+ }
+
+ /**
+ * Handle swipes in any direction where the result is a scroll event. This call blocks
+ * until the UI has fired a scroll event or timeout.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @return true if we are not at the beginning or end of the scrollable view.
+ */
+ public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
+ final int steps) {
+ Log.d(LOG_TAG, "scrollSwipe (" + downX + ", " + downY + ", " + upX + ", "
+ + upY + ", " + steps +")");
+
+ Runnable command = new Runnable() {
+ @Override
+ public void run() {
+ swipe(downX, downY, upX, upY, steps);
+ }
+ };
+
+ // Collect all accessibility events generated during the swipe command and get the
+ // last event
+ ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
+ runAndWaitForEvents(command,
+ new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
+ Configurator.getInstance().getScrollAcknowledgmentTimeout());
+
+ AccessibilityEvent event = getLastMatchingEvent(events,
+ AccessibilityEvent.TYPE_VIEW_SCROLLED);
+
+ if (event == null) {
+ // end of scroll since no new scroll events received
+ recycleAccessibilityEvents(events);
+ return false;
+ }
+
+ // AdapterViews have indices we can use to check for the beginning.
+ boolean foundEnd = false;
+ if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
+ foundEnd = event.getFromIndex() == 0 ||
+ (event.getItemCount() - 1) == event.getToIndex();
+ Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
+ } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
+ // Determine if we are scrolling vertically or horizontally.
+ if (downX == upX) {
+ // Vertical
+ foundEnd = event.getScrollY() == 0 ||
+ event.getScrollY() == event.getMaxScrollY();
+ Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
+ } else if (downY == upY) {
+ // Horizontal
+ foundEnd = event.getScrollX() == 0 ||
+ event.getScrollX() == event.getMaxScrollX();
+ Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
+ }
+ }
+ recycleAccessibilityEvents(events);
+ return !foundEnd;
+ }
+
+ private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
+ for (int x = events.size(); x > 0; x--) {
+ AccessibilityEvent event = events.get(x - 1);
+ if (event.getEventType() == type)
+ return event;
+ }
+ return null;
+ }
+
+ private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
+ for (AccessibilityEvent event : events)
+ event.recycle();
+ events.clear();
+ }
+
+ /**
+ * Handle swipes in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @return true if the swipe executed successfully
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
+ return swipe(downX, downY, upX, upY, steps, false /*drag*/);
+ }
+
+ /**
+ * Handle swipes/drags in any direction.
+ * @param downX
+ * @param downY
+ * @param upX
+ * @param upY
+ * @param steps
+ * @param drag when true, the swipe becomes a drag swipe
+ * @return true if the swipe executed successfully
+ */
+ public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
+ boolean ret = false;
+ int swipeSteps = steps;
+ double xStep = 0;
+ double yStep = 0;
+
+ // avoid a divide by zero
+ if(swipeSteps == 0)
+ swipeSteps = 1;
+
+ xStep = ((double)(upX - downX)) / swipeSteps;
+ yStep = ((double)(upY - downY)) / swipeSteps;
+
+ // first touch starts exactly at the point requested
+ ret = touchDown(downX, downY);
+ if (drag)
+ SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
+ for(int i = 1; i < swipeSteps; i++) {
+ ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
+ if(ret == false)
+ break;
+ // set some known constant delay between steps as without it this
+ // become completely dependent on the speed of the system and results
+ // may vary on different devices. This guarantees at minimum we have
+ // a preset delay.
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+ if (drag)
+ SystemClock.sleep(REGULAR_CLICK_LENGTH);
+ ret &= touchUp(upX, upY);
+ return(ret);
+ }
+
+ /**
+ * Performs a swipe between points in the Point array.
+ * @param segments is Point array containing at least one Point object
+ * @param segmentSteps steps to inject between two Points
+ * @return true on success
+ */
+ public boolean swipe(Point[] segments, int segmentSteps) {
+ boolean ret = false;
+ int swipeSteps = segmentSteps;
+ double xStep = 0;
+ double yStep = 0;
+
+ // avoid a divide by zero
+ if(segmentSteps == 0)
+ segmentSteps = 1;
+
+ // must have some points
+ if(segments.length == 0)
+ return false;
+
+ // first touch starts exactly at the point requested
+ ret = touchDown(segments[0].x, segments[0].y);
+ for(int seg = 0; seg < segments.length; seg++) {
+ if(seg + 1 < segments.length) {
+
+ xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps;
+ yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps;
+
+ for(int i = 1; i < swipeSteps; i++) {
+ ret &= touchMove(segments[seg].x + (int)(xStep * i),
+ segments[seg].y + (int)(yStep * i));
+ if(ret == false)
+ break;
+ // set some known constant delay between steps as without it this
+ // become completely dependent on the speed of the system and results
+ // may vary on different devices. This guarantees at minimum we have
+ // a preset delay.
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+ }
+ }
+ ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y);
+ return(ret);
+ }
+
+
+ public boolean sendText(String text) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "sendText (" + text + ")");
+ }
+
+ KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
+
+ if (events != null) {
+ long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
+ for (KeyEvent event2 : events) {
+ // We have to change the time of an event before injecting it because
+ // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+ // time stamp and the system rejects too old events. Hence, it is
+ // possible for an event to become stale before it is injected if it
+ // takes too long to inject the preceding ones.
+ KeyEvent event = KeyEvent.changeTimeRepeat(event2,
+ SystemClock.uptimeMillis(), 0);
+ if (!injectEventSync(event)) {
+ return false;
+ }
+ SystemClock.sleep(keyDelay);
+ }
+ }
+ return true;
+ }
+
+ public boolean sendKey(int keyCode, int metaState) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
+ }
+
+ final long eventTime = SystemClock.uptimeMillis();
+ KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if (injectEventSync(downEvent)) {
+ KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
+ keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+ InputDevice.SOURCE_KEYBOARD);
+ if(injectEventSync(upEvent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Rotates right and also freezes rotation in that position by
+ * disabling the sensors. If you want to un-freeze the rotation
+ * and re-enable the sensors see {@link #unfreezeRotation()}. Note
+ * that doing so may cause the screen contents to rotate
+ * depending on the current physical position of the test device.
+ * @throws RemoteException
+ */
+ public void setRotationRight() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270);
+ }
+
+ /**
+ * Rotates left and also freezes rotation in that position by
+ * disabling the sensors. If you want to un-freeze the rotation
+ * and re-enable the sensors see {@link #unfreezeRotation()}. Note
+ * that doing so may cause the screen contents to rotate
+ * depending on the current physical position of the test device.
+ * @throws RemoteException
+ */
+ public void setRotationLeft() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90);
+ }
+
+ /**
+ * Rotates up and also freezes rotation in that position by
+ * disabling the sensors. If you want to un-freeze the rotation
+ * and re-enable the sensors see {@link #unfreezeRotation()}. Note
+ * that doing so may cause the screen contents to rotate
+ * depending on the current physical position of the test device.
+ * @throws RemoteException
+ */
+ public void setRotationNatural() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0);
+ }
+
+ /**
+ * Disables the sensors and freezes the device rotation at its
+ * current rotation state.
+ * @throws RemoteException
+ */
+ public void freezeRotation() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
+ }
+
+ /**
+ * Re-enables the sensors and un-freezes the device rotation
+ * allowing its contents to rotate with the device physical rotation.
+ * @throws RemoteException
+ */
+ public void unfreezeRotation() {
+ mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE);
+ }
+
+ /**
+ * This method simply presses the power button if the screen is OFF else
+ * it does nothing if the screen is already ON.
+ * @return true if the device was asleep else false
+ * @throws RemoteException
+ */
+ public boolean wakeDevice() throws RemoteException {
+ if(!isScreenOn()) {
+ sendKey(KeyEvent.KEYCODE_POWER, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This method simply presses the power button if the screen is ON else
+ * it does nothing if the screen is already OFF.
+ * @return true if the device was awake else false
+ * @throws RemoteException
+ */
+ public boolean sleepDevice() throws RemoteException {
+ if(isScreenOn()) {
+ this.sendKey(KeyEvent.KEYCODE_POWER, 0);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks the power manager if the screen is ON
+ * @return true if the screen is ON else false
+ * @throws RemoteException
+ */
+ public boolean isScreenOn() throws RemoteException {
+ return mUiAutomatorBridge.isScreenOn();
+ }
+
+ private boolean injectEventSync(InputEvent event) {
+ return mUiAutomatorBridge.injectInputEvent(event, true);
+ }
+
+ private int getPointerAction(int motionEnvent, int index) {
+ return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
+ }
+
+ /**
+ * Performs a multi-touch gesture
+ *
+ * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
+ * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
+ * to specify the touch points along the path of a pointer, the caller is able to specify
+ * complex gestures like circles, irregular shapes etc, where each pointer may take a
+ * different path.
+ *
+ * To create a single point on a pointer's touch path
+ * <code>
+ * PointerCoords p = new PointerCoords();
+ * p.x = stepX;
+ * p.y = stepY;
+ * p.pressure = 1;
+ * p.size = 1;
+ * </code>
+ * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
+ * Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
+ * path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
+ * @since API Level 18
+ */
+ public void generateMultiPointerGesture(PointerCoords[] ... touches) {
+ if (touches.length < 2) {
+ throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
+ }
+
+ // Get the pointer with the max steps to inject.
+ int maxSteps = 0;
+ for (int x = 0; x < touches.length; x++)
+ maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps;
+
+ // specify the properties for each pointer as finger touch
+ PointerProperties[] properties = new PointerProperties[touches.length];
+ PointerCoords[] pointerCoords = new PointerCoords[touches.length];
+ for (int x = 0; x < touches.length; x++) {
+ PointerProperties prop = new PointerProperties();
+ prop.id = x;
+ prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
+ properties[x] = prop;
+
+ // for each pointer set the first coordinates for touch down
+ pointerCoords[x] = touches[x][0];
+ }
+
+ // Touch down all pointers
+ long downTime = SystemClock.uptimeMillis();
+ MotionEvent event;
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
+ properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ injectEventSync(event);
+
+ for (int x = 1; x < touches.length; x++) {
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
+ pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ injectEventSync(event);
+ }
+
+ // Move all pointers
+ for (int i = 1; i < maxSteps - 1; i++) {
+ // for each pointer
+ for (int x = 0; x < touches.length; x++) {
+ // check if it has coordinates to move
+ if (touches[x].length > i)
+ pointerCoords[x] = touches[x][i];
+ else
+ pointerCoords[x] = touches[x][touches[x].length - 1];
+ }
+
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
+ 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+
+ injectEventSync(event);
+ SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
+ }
+
+ // For each pointer get the last coordinates
+ for (int x = 0; x < touches.length; x++)
+ pointerCoords[x] = touches[x][touches[x].length - 1];
+
+ // touch up
+ for (int x = 1; x < touches.length; x++) {
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
+ getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
+ pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ injectEventSync(event);
+ }
+
+ Log.i(LOG_TAG, "x " + pointerCoords[0].x);
+ // first to touch down is last up
+ event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
+ properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
+ injectEventSync(event);
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/QueryController.java b/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
similarity index 98%
rename from uiautomator/library/src/com/android/uiautomator/core/QueryController.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
index 0af603a..6931528 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/QueryController.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/QueryController.java
@@ -15,12 +15,12 @@
*/
package com.android.uiautomator.core;
+import android.app.UiAutomation.OnAccessibilityEventListener;
import android.os.SystemClock;
import android.util.Log;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
-import com.android.uiautomator.core.UiAutomatorBridge.AccessibilityEventListener;
/**
* The QueryController main purpose is to translate a {@link UiSelector} selectors to
@@ -55,7 +55,7 @@
public QueryController(UiAutomatorBridge bridge) {
mUiAutomatorBridge = bridge;
- bridge.addAccessibilityEventListener(new AccessibilityEventListener() {
+ bridge.setOnAccessibilityEventListener(new OnAccessibilityEventListener() {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
synchronized (mLock) {
@@ -169,7 +169,7 @@
final long waitInterval = 250;
AccessibilityNodeInfo rootNode = null;
for(int x = 0; x < maxRetry; x++) {
- rootNode = mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow();
+ rootNode = mUiAutomatorBridge.getRootInActiveWindow();
if (rootNode != null) {
return rootNode;
}
@@ -480,7 +480,7 @@
}
public AccessibilityNodeInfo getAccessibilityRootNode() {
- return mUiAutomatorBridge.getRootAccessibilityNodeInfoInActiveWindow();
+ return mUiAutomatorBridge.getRootInActiveWindow();
}
/**
diff --git a/uiautomator/library/src/com/android/uiautomator/core/Tracer.java b/uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/core/Tracer.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/Tracer.java
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java
new file mode 100644
index 0000000..04bbb34
--- /dev/null
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiAutomatorBridge.java
@@ -0,0 +1,129 @@
+package com.android.uiautomator.core;
+
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.app.UiAutomation.OnAccessibilityEventListener;
+import android.graphics.Bitmap;
+import android.util.Log;
+import android.view.Display;
+import android.view.InputEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @hide
+ */
+public abstract class UiAutomatorBridge {
+
+ private static final String LOG_TAG = UiAutomatorBridge.class.getSimpleName();
+
+ /**
+ * This value has the greatest bearing on the appearance of test execution speeds.
+ * This value is used as the minimum time to wait before considering the UI idle after
+ * each action.
+ */
+ private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;//ms
+
+ /**
+ * This is the maximum time the automation will wait for the UI to go idle. Execution
+ * will resume normally anyway. This is to prevent waiting forever on display updates
+ * that may be related to spinning wheels or progress updates of sorts etc...
+ */
+ private static final long TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE = 1000 * 10;//ms
+
+ private final UiAutomation mUiAutomation;
+
+ private final InteractionController mInteractionController;
+
+ private final QueryController mQueryController;
+
+ UiAutomatorBridge(UiAutomation uiAutomation) {
+ mUiAutomation = uiAutomation;
+ mInteractionController = new InteractionController(this);
+ mQueryController = new QueryController(this);
+ }
+
+ InteractionController getInteractionController() {
+ return mInteractionController;
+ }
+
+ QueryController getQueryController() {
+ return mQueryController;
+ }
+
+ public void setOnAccessibilityEventListener(OnAccessibilityEventListener listener) {
+ mUiAutomation.setOnAccessibilityEventListener(listener);
+ }
+
+ public AccessibilityNodeInfo getRootInActiveWindow() {
+ return mUiAutomation.getRootInActiveWindow();
+ }
+
+ public boolean injectInputEvent(InputEvent event, boolean sync) {
+ return mUiAutomation.injectInputEvent(event, sync);
+ }
+
+ public boolean setRotation(int rotation) {
+ return mUiAutomation.setRotation(rotation);
+ }
+
+ public abstract int getRotation();
+
+ public abstract boolean isScreenOn();
+
+ public void waitForIdle() {
+ waitForIdle(TOTAL_TIME_TO_WAIT_FOR_IDLE_STATE);
+ }
+
+ public void waitForIdle(long timeout) {
+ try {
+ mUiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeout);
+ } catch (TimeoutException te) {
+ Log.w(LOG_TAG, "Could not detect idle state.", te);
+ }
+ }
+
+ public AccessibilityEvent executeCommandAndWaitForAccessibilityEvent(Runnable command,
+ AccessibilityEventFilter filter, long timeoutMillis) throws TimeoutException {
+ return mUiAutomation.executeAndWaitForEvent(command,
+ filter, timeoutMillis);
+ }
+
+ public boolean takeScreenshot(File storePath, int quality) {
+ Bitmap screenshot = mUiAutomation.takeScreenshot();
+ if (screenshot == null) {
+ return false;
+ }
+ BufferedOutputStream bos = null;
+ try {
+ bos = new BufferedOutputStream(new FileOutputStream(storePath));
+ if (bos != null) {
+ screenshot.compress(Bitmap.CompressFormat.PNG, quality, bos);
+ bos.flush();
+ }
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "failed to save screen shot to file", ioe);
+ return false;
+ } finally {
+ if (bos != null) {
+ try {
+ bos.close();
+ } catch (IOException ioe) {
+ /* ignore */
+ }
+ }
+ screenshot.recycle();
+ }
+ return true;
+ }
+
+ public abstract Display getDefaultDisplay();
+
+ public abstract long getSystemLongPressTime();
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiCollection.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/core/UiCollection.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiCollection.java
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
similarity index 84%
rename from uiautomator/library/src/com/android/uiautomator/core/UiDevice.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
index b668bea..5389590 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/UiDevice.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
@@ -16,18 +16,15 @@
package com.android.uiautomator.core;
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
import android.graphics.Point;
-import android.hardware.display.DisplayManagerGlobal;
import android.os.Build;
import android.os.Environment;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
-import android.os.Trace;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
@@ -37,11 +34,8 @@
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.util.Predicate;
import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -56,8 +50,6 @@
public class UiDevice {
private static final String LOG_TAG = UiDevice.class.getSimpleName();
- private static final long DEFAULT_TIMEOUT_MILLIS = 10 * 1000;
-
// Sometimes HOME and BACK key presses will generate no events if already on
// home page or there is nothing to go back to, Set low timeouts.
private static final long KEY_PRESS_EVENT_TIMEOUT = 1 * 1000;
@@ -70,14 +62,20 @@
private boolean mInWatcherContext = false;
// provides access the {@link QueryController} and {@link InteractionController}
- private final UiAutomatorBridge mUiAutomationBridge;
+ private UiAutomatorBridge mUiAutomationBridge;
// reference to self
- private static UiDevice mDevice;
+ private static UiDevice sDevice;
private UiDevice() {
- mUiAutomationBridge = new UiAutomatorBridge();
- mDevice = this;
+ /* hide constructor */
+ }
+
+ /**
+ * @hide
+ */
+ public void initialize(UiAutomatorBridge uiAutomatorBridge) {
+ mUiAutomationBridge = uiAutomatorBridge;
}
boolean isInWatcherContext() {
@@ -86,11 +84,12 @@
/**
* Provides access the {@link QueryController} and {@link InteractionController}
- * @return {@link UiAutomatorBridge}
+ * @return {@link ShellUiAutomatorBridge}
*/
UiAutomatorBridge getAutomatorBridge() {
return mUiAutomationBridge;
}
+
/**
* Retrieves a singleton instance of UiDevice
*
@@ -98,27 +97,27 @@
* @since API Level 16
*/
public static UiDevice getInstance() {
- if (mDevice == null) {
- mDevice = new UiDevice();
+ if (sDevice == null) {
+ sDevice = new UiDevice();
}
- return mDevice;
+ return sDevice;
}
/**
* Returns the display size in dp (device-independent pixel)
*
- * The returned display size is adjusted per screen rotation
+ * The returned display size is adjusted per screen rotation. Also this will return the actual
+ * size of the screen, rather than adjusted per system decorations (like status bar).
*
* @return a Point containing the display size in dp
- * @hide
*/
public Point getDisplaySizeDp() {
Tracer.trace();
- Display display = getDefaultDisplay();
+ Display display = mUiAutomationBridge.getDefaultDisplay();
Point p = new Point();
- display.getSize(p);
+ display.getRealSize(p);
DisplayMetrics metrics = new DisplayMetrics();
- display.getMetrics(metrics);
+ display.getRealMetrics(metrics);
float dpx = p.x / metrics.density;
float dpy = p.y / metrics.density;
p.x = Math.round(dpx);
@@ -345,7 +344,7 @@
*/
public int getDisplayWidth() {
Tracer.trace();
- Display display = getDefaultDisplay();
+ Display display = mUiAutomationBridge.getDefaultDisplay();
Point p = new Point();
display.getSize(p);
return p.x;
@@ -359,7 +358,7 @@
*/
public int getDisplayHeight() {
Tracer.trace();
- Display display = getDefaultDisplay();
+ Display display = mUiAutomationBridge.getDefaultDisplay();
Point p = new Point();
display.getSize(p);
return p.y;
@@ -378,7 +377,7 @@
if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
return (false);
}
- return getAutomatorBridge().getInteractionController().click(x, y);
+ return getAutomatorBridge().getInteractionController().clickNoSync(x, y);
}
/**
@@ -397,7 +396,26 @@
public boolean swipe(int startX, int startY, int endX, int endY, int steps) {
Tracer.trace(startX, startY, endX, endY, steps);
return mUiAutomationBridge.getInteractionController()
- .scrollSwipe(startX, startY, endX, endY, steps);
+ .swipe(startX, startY, endX, endY, steps);
+ }
+
+ /**
+ * Performs a swipe from one coordinate to another using the number of steps
+ * to determine smoothness and speed. Each step execution is throttled to 5ms
+ * per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
+ *
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ * @param steps is the number of move steps sent to the system
+ * @return false if the operation fails or the coordinates are invalid
+ * @since API Level 18
+ */
+ public boolean drag(int startX, int startY, int endX, int endY, int steps) {
+ Tracer.trace(startX, startY, endX, endY, steps);
+ return mUiAutomationBridge.getInteractionController()
+ .swipe(startX, startY, endX, endY, steps, true);
}
/**
@@ -421,7 +439,7 @@
*/
public void waitForIdle() {
Tracer.trace();
- waitForIdle(DEFAULT_TIMEOUT_MILLIS);
+ waitForIdle(Configurator.getInstance().getWaitForIdleTimeout());
}
/**
@@ -574,9 +592,9 @@
*/
public boolean isNaturalOrientation() {
Tracer.trace();
- Display display = getDefaultDisplay();
- return display.getRotation() == Surface.ROTATION_0 ||
- display.getRotation() == Surface.ROTATION_180;
+ int ret = mUiAutomationBridge.getRotation();
+ return ret == UiAutomation.ROTATION_FREEZE_0 ||
+ ret == UiAutomation.ROTATION_FREEZE_180;
}
/**
@@ -585,7 +603,8 @@
*/
public int getDisplayRotation() {
Tracer.trace();
- return getDefaultDisplay().getRotation();
+ waitForIdle();
+ return mUiAutomationBridge.getRotation();
}
/**
@@ -706,9 +725,12 @@
AccessibilityNodeInfo root =
getAutomatorBridge().getQueryController().getAccessibilityRootNode();
if(root != null) {
- AccessibilityNodeInfoDumper.dumpWindowToFile(
- root, new File(new File(Environment.getDataDirectory(),
- "local/tmp"), fileName));
+ Display display = mUiAutomationBridge.getDefaultDisplay();
+ Point size = new Point();
+ display.getSize(size);
+ AccessibilityNodeInfoDumper.dumpWindowToFile(root,
+ new File(new File(Environment.getDataDirectory(), "local/tmp"), fileName),
+ display.getRotation(), size.x, size.y);
}
}
@@ -738,9 +760,9 @@
public void run() {
}
};
- Predicate<AccessibilityEvent> checkWindowUpdate = new Predicate<AccessibilityEvent>() {
+ AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() {
@Override
- public boolean apply(AccessibilityEvent t) {
+ public boolean accept(AccessibilityEvent t) {
if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
return packageName == null || packageName.equals(t.getPackageName());
}
@@ -759,25 +781,6 @@
return true;
}
- private static Display getDefaultDisplay() {
- return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
- }
-
- /**
- * @return the current display rotation in degrees
- */
- private static float getDegreesForRotation(int value) {
- switch (value) {
- case Surface.ROTATION_90:
- return 360f - 90f;
- case Surface.ROTATION_180:
- return 360f - 180f;
- case Surface.ROTATION_270:
- return 360f - 270f;
- }
- return 0f;
- }
-
/**
* Take a screenshot of current window and store it as PNG
*
@@ -806,66 +809,6 @@
*/
public boolean takeScreenshot(File storePath, float scale, int quality) {
Tracer.trace(storePath, scale, quality);
- // This is from com.android.systemui.screenshot.GlobalScreenshot#takeScreenshot
- // We need to orient the screenshot correctly (and the Surface api seems to take screenshots
- // only in the natural orientation of the device :!)
- DisplayMetrics displayMetrics = new DisplayMetrics();
- Display display = getDefaultDisplay();
- display.getRealMetrics(displayMetrics);
- float[] dims = {displayMetrics.widthPixels, displayMetrics.heightPixels};
- float degrees = getDegreesForRotation(display.getRotation());
- boolean requiresRotation = (degrees > 0);
- Matrix matrix = new Matrix();
- matrix.reset();
- if (scale != 1.0f) {
- matrix.setScale(scale, scale);
- }
- if (requiresRotation) {
- // Get the dimensions of the device in its native orientation
- matrix.preRotate(-degrees);
- }
- matrix.mapPoints(dims);
- dims[0] = Math.abs(dims[0]);
- dims[1] = Math.abs(dims[1]);
-
- // Take the screenshot
- Bitmap screenShot = Surface.screenshot((int) dims[0], (int) dims[1]);
- if (screenShot == null) {
- return false;
- }
-
- if (requiresRotation) {
- // Rotate the screenshot to the current orientation
- int width = displayMetrics.widthPixels;
- int height = displayMetrics.heightPixels;
- if (scale != 1.0f) {
- width = Math.round(scale * width);
- height = Math.round(scale * height);
- }
- Bitmap ss = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Canvas c = new Canvas(ss);
- c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
- c.rotate(degrees);
- c.translate(-dims[0] / 2, -dims[1] / 2);
- c.drawBitmap(screenShot, 0, 0, null);
- c.setBitmap(null);
- screenShot = ss;
- }
-
- // Optimizations
- screenShot.setHasAlpha(false);
-
- try {
- FileOutputStream fos = new FileOutputStream(storePath);
- screenShot.compress(Bitmap.CompressFormat.PNG, quality, fos);
- fos.flush();
- fos.close();
- } catch (IOException ioe) {
- Log.e(LOG_TAG, "failed to save screen shot to file", ioe);
- return false;
- } finally {
- screenShot.recycle();
- }
- return true;
+ return mUiAutomationBridge.takeScreenshot(storePath, quality);
}
}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiObject.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
similarity index 68%
rename from uiautomator/library/src/com/android/uiautomator/core/UiObject.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
index 2bf6455..cf66cca 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/UiObject.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
@@ -16,11 +16,12 @@
package com.android.uiautomator.core;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Log;
import android.view.KeyEvent;
-import android.view.accessibility.AccessibilityEvent;
+import android.view.MotionEvent.PointerCoords;
import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -35,7 +36,9 @@
private static final String LOG_TAG = UiObject.class.getSimpleName();
/**
* @since API Level 16
+ * @deprecated use {@link Configurator#setWaitForSelectorTimeout(long)}
**/
+ @Deprecated
protected static final long WAIT_FOR_SELECTOR_TIMEOUT = 10 * 1000;
/**
* @since API Level 16
@@ -47,17 +50,25 @@
**/
protected static final long WAIT_FOR_WINDOW_TMEOUT = 5500;
/**
- * @since API Level 17
- **/
- protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000;
- /**
* @since API Level 16
**/
protected static final int SWIPE_MARGIN_LIMIT = 5;
+ /**
+ * @since API Level 17
+ * @deprecated use {@link Configurator#setScrollAcknowledgmentTimeout(long)}
+ **/
+ @Deprecated
+ protected static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000;
+ /**
+ * @since API Level 18
+ **/
+ protected static final int FINGER_TOUCH_HALF_WIDTH = 20;
private final UiSelector mSelector;
private final UiAutomatorBridge mUiAutomationBridge;
+ private final Configurator mConfig = Configurator.getInstance();
+
/**
* Constructs a UiObject to represent a specific UI element matched by the specified
* {@link UiSelector} selector properties.
@@ -140,7 +151,7 @@
*/
public int getChildCount() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -183,6 +194,42 @@
}
/**
+ * Performs a drag of this object to a destination UiObject. Note that the number of steps
+ * used can influence the drag speed and varying speeds may impact the results. Consider
+ * evaluating different speeds when testing this method.
+ *
+ * @param destObj
+ * @param steps usually 40 steps. More or less to change the speed.
+ * @return true of successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(UiObject destObj, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ Rect dstRect = destObj.getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(),
+ dstRect.centerX(), dstRect.centerY(), steps, true);
+ }
+
+ /**
+ * Performs a drag of this object to arbitrary coordinates. Note that the number of steps
+ * used will influence the drag speed and varying speeds may impact the results. Consider
+ * evaluating different speeds when testing this method.
+ *
+ * @param destX
+ * @param destY
+ * @param steps
+ * @return true of successful
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public boolean dragTo(int destX, int destY, int steps) throws UiObjectNotFoundException {
+ Rect srcRect = getVisibleBounds();
+ return getInteractionController().swipe(srcRect.centerX(), srcRect.centerY(), destX, destY,
+ steps, true);
+ }
+
+ /**
* Perform the action on the UI element that is represented by this UiObject. Also see
* {@link UiScrollable#scrollToBeginning(int)}, {@link UiScrollable#scrollToEnd(int)},
* {@link UiScrollable#scrollBackward()}, {@link UiScrollable#scrollForward()}.
@@ -285,7 +332,9 @@
}
// targeted node's bounds
- Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node);
+ int w = UiDevice.getInstance().getDisplayWidth();
+ int h = UiDevice.getInstance().getDisplayHeight();
+ Rect nodeRect = AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node, w, h);
// is the targeted node within a scrollable container?
AccessibilityNodeInfo scrollableParentNode = getScrollableParent(node);
@@ -296,7 +345,7 @@
// Scrollable parent's visible bounds
Rect parentRect = AccessibilityNodeInfoHelper
- .getVisibleBoundsInScreen(scrollableParentNode);
+ .getVisibleBoundsInScreen(scrollableParentNode, w, h);
// adjust for partial clipping of targeted by parent node if required
nodeRect.intersect(parentRect);
return nodeRect;
@@ -332,14 +381,13 @@
*/
public boolean click() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().clickAndWaitForEvents(rect.centerX(), rect.centerY(),
- WAIT_FOR_EVENT_TMEOUT, false, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED +
- AccessibilityEvent.TYPE_VIEW_SELECTED);
+ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY(),
+ mConfig.getActionAcknowledgmentTimeout());
}
/**
@@ -373,13 +421,13 @@
*/
public boolean clickAndWaitForNewWindow(long timeout) throws UiObjectNotFoundException {
Tracer.trace(timeout);
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().clickAndWaitForNewWindow(
- rect.centerX(), rect.centerY(), timeout);
+ return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY(),
+ mConfig.getActionAcknowledgmentTimeout());
}
/**
@@ -391,12 +439,12 @@
*/
public boolean clickTopLeft() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().click(rect.left + 5, rect.top + 5);
+ return getInteractionController().clickNoSync(rect.left + 5, rect.top + 5);
}
/**
@@ -408,12 +456,12 @@
*/
public boolean longClickBottomRight() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.right - 5, rect.bottom - 5);
+ return getInteractionController().longTapNoSync(rect.right - 5, rect.bottom - 5);
}
/**
@@ -425,12 +473,12 @@
*/
public boolean clickBottomRight() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().click(rect.right - 5, rect.bottom - 5);
+ return getInteractionController().clickNoSync(rect.right - 5, rect.bottom - 5);
}
/**
@@ -442,12 +490,12 @@
*/
public boolean longClick() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.centerX(), rect.centerY());
+ return getInteractionController().longTapNoSync(rect.centerX(), rect.centerY());
}
/**
@@ -459,12 +507,12 @@
*/
public boolean longClickTopLeft() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.left + 5, rect.top + 5);
+ return getInteractionController().longTapNoSync(rect.left + 5, rect.top + 5);
}
/**
@@ -476,7 +524,7 @@
*/
public String getText() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -486,6 +534,24 @@
}
/**
+ * Reads the <code>className</code> property of the UI element
+ *
+ * @return class name of the current node represented by this UiObject
+ * @throws UiObjectNotFoundException if no match could be found
+ * @since API Level 18
+ */
+ public String getClassName() throws UiObjectNotFoundException {
+ Tracer.trace();
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if(node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+ String retVal = safeStringReturn(node.getClassName());
+ Log.d(LOG_TAG, String.format("getClassName() = %s", retVal));
+ return retVal;
+ }
+
+ /**
* Reads the <code>content_desc</code> property of the UI element
*
* @return value of node attribute "content_desc"
@@ -494,7 +560,7 @@
*/
public String getContentDescription() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -546,12 +612,12 @@
public void clearTextField() throws UiObjectNotFoundException {
Tracer.trace();
// long click left + center
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- getInteractionController().longTap(rect.left + 20, rect.centerY());
+ getInteractionController().longTapNoSync(rect.left + 20, rect.centerY());
// check if the edit menu is open
UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all"));
if(selectAll.waitForExists(50))
@@ -570,7 +636,7 @@
*/
public boolean isChecked() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -586,7 +652,7 @@
*/
public boolean isSelected() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -602,7 +668,7 @@
*/
public boolean isCheckable() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -618,7 +684,7 @@
*/
public boolean isEnabled() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -634,7 +700,7 @@
*/
public boolean isClickable() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -650,7 +716,7 @@
*/
public boolean isFocused() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -666,7 +732,7 @@
*/
public boolean isFocusable() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -682,7 +748,7 @@
*/
public boolean isScrollable() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -698,7 +764,7 @@
*/
public boolean isLongClickable() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -714,7 +780,7 @@
*/
public String getPackageName() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -734,7 +800,7 @@
*/
public Rect getVisibleBounds() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -750,7 +816,7 @@
*/
public Rect getBounds() throws UiObjectNotFoundException {
Tracer.trace();
- AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT);
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
if(node == null) {
throw new UiObjectNotFoundException(getSelector().toString());
}
@@ -832,4 +898,175 @@
return "";
return cs.toString();
}
+
+ /**
+ * PinchOut generates a 2 pointer gesture where each pointer is moving from the center out
+ * away from each other diagonally towards the edges of the current UI element represented by
+ * this UiObject.
+ * @param percent of the object's diagonal length to use for the pinch
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public void pinchOut(int percent, int steps) throws UiObjectNotFoundException {
+ // make value between 1 and 100
+ percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+ float percentage = percent / 100f;
+
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if (node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+
+ Rect rect = getVisibleBounds(node);
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
+ throw new IllegalStateException("Object width is too small for operation");
+
+ // start from the same point at the center of the control
+ Point startPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+ Point startPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+
+ // End at the top-left and bottom-right corners of the control
+ Point endPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
+ rect.centerY());
+ Point endPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
+ rect.centerY());
+
+ twoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ /**
+ * PinchIn generates a 2 pointer gesture where each pointer is moving towards the other
+ * diagonally from the edges of the current UI element represented by this UiObject, until the
+ * center.
+ * @param percent of the object's diagonal length to use for the pinch
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @throws UiObjectNotFoundException
+ * @since API Level 18
+ */
+ public void pinchIn(int percent, int steps) throws UiObjectNotFoundException {
+ // make value between 1 and 100
+ percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent;
+ float percentage = percent / 100f;
+
+ AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
+ if (node == null) {
+ throw new UiObjectNotFoundException(getSelector().toString());
+ }
+
+ Rect rect = getVisibleBounds(node);
+ if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
+ throw new IllegalStateException("Object width is too small for operation");
+
+ Point startPoint1 = new Point(rect.centerX() - (int)((rect.width()/2) * percentage),
+ rect.centerY());
+ Point startPoint2 = new Point(rect.centerX() + (int)((rect.width()/2) * percentage),
+ rect.centerY());
+
+ Point endPoint1 = new Point(rect.centerX() - FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+ Point endPoint2 = new Point(rect.centerX() + FINGER_TOUCH_HALF_WIDTH, rect.centerY());
+
+ twoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
+ }
+
+ /**
+ * Generates a 2 pointer gesture from an arbitrary starting and ending points.
+ *
+ * @param startPoint1 start point of pointer 1
+ * @param startPoint2 start point of pointer 2
+ * @param endPoint1 end point of pointer 1
+ * @param endPoint2 end point of pointer 2
+ * @param steps indicates the number of injected move steps into the system. Steps are
+ * injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
+ * @since API Level 18
+ */
+ public void twoPointerGesture(Point startPoint1, Point startPoint2, Point endPoint1,
+ Point endPoint2, int steps) {
+
+ // avoid a divide by zero
+ if(steps == 0)
+ steps = 1;
+
+ final float stepX1 = (endPoint1.x - startPoint1.x) / steps;
+ final float stepY1 = (endPoint1.y - startPoint1.y) / steps;
+ final float stepX2 = (endPoint2.x - startPoint2.x) / steps;
+ final float stepY2 = (endPoint2.y - startPoint2.y) / steps;
+
+ int eventX1, eventY1, eventX2, eventY2;
+ eventX1 = startPoint1.x;
+ eventY1 = startPoint1.y;
+ eventX2 = startPoint2.x;
+ eventY2 = startPoint2.y;
+
+ // allocate for steps plus first down and last up
+ PointerCoords[] points1 = new PointerCoords[steps + 2];
+ PointerCoords[] points2 = new PointerCoords[steps + 2];
+
+ // Include the first and last touch downs in the arrays of steps
+ for (int i = 0; i < steps + 1; i++) {
+ PointerCoords p1 = new PointerCoords();
+ p1.x = eventX1;
+ p1.y = eventY1;
+ p1.pressure = 1;
+ p1.size = 1;
+ points1[i] = p1;
+
+ PointerCoords p2 = new PointerCoords();
+ p2.x = eventX2;
+ p2.y = eventY2;
+ p2.pressure = 1;
+ p2.size = 1;
+ points2[i] = p2;
+
+ eventX1 += stepX1;
+ eventY1 += stepY1;
+ eventX2 += stepX2;
+ eventY2 += stepY2;
+ }
+
+ // ending pointers coordinates
+ PointerCoords p1 = new PointerCoords();
+ p1.x = endPoint1.x;
+ p1.y = endPoint1.y;
+ p1.pressure = 1;
+ p1.size = 1;
+ points1[steps + 1] = p1;
+
+ PointerCoords p2 = new PointerCoords();
+ p2.x = endPoint2.x;
+ p2.y = endPoint2.y;
+ p2.pressure = 1;
+ p2.size = 1;
+ points2[steps + 1] = p2;
+
+ multiPointerGesture(points1, points2);
+ }
+
+ /**
+ * Performs a multi-touch gesture
+ *
+ * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
+ * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
+ * to specify the touch points along the path of a pointer, the caller is able to specify
+ * complex gestures like circles, irregular shapes etc, where each pointer may take a
+ * different path.
+ *
+ * To create a single point on a pointer's touch path
+ * <code>
+ * PointerCoords p = new PointerCoords();
+ * p.x = stepX;
+ * p.y = stepY;
+ * p.pressure = 1;
+ * p.size = 1;
+ * </code>
+ * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
+ * Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
+ * path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
+ * @since API Level 18
+ */
+ public void multiPointerGesture(PointerCoords[] ...touches) {
+ getInteractionController().generateMultiPointerGesture(touches);
+ }
}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/core/UiObjectNotFoundException.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiObjectNotFoundException.java
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/core/UiScrollable.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiScrollable.java
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
similarity index 94%
rename from uiautomator/library/src/com/android/uiautomator/core/UiSelector.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
index 8963c38..c1ddb32 100644
--- a/uiautomator/library/src/com/android/uiautomator/core/UiSelector.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
@@ -58,6 +58,8 @@
static final int SELECTOR_CLASS_REGEX = 26;
static final int SELECTOR_DESCRIPTION_REGEX = 27;
static final int SELECTOR_PACKAGE_NAME_REGEX = 28;
+ static final int SELECTOR_RESOURCE_ID = 29;
+ static final int SELECTOR_CHECKABLE = 30;
private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>();
@@ -279,6 +281,17 @@
}
/**
+ * Set the search criteria to match the given resource id.
+ *
+ * @param id value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 18
+ */
+ public UiSelector resourceId(String id) {
+ return buildSelector(SELECTOR_RESOURCE_ID, id);
+ }
+
+ /**
* Set the search criteria to match the widget by its node
* index in the layout hierarchy.
*
@@ -458,6 +471,25 @@
}
/**
+ * Set the search criteria to match widgets that are checkable.
+ *
+ * Typically, using this search criteria alone is not useful.
+ * You should also include additional criteria, such as text,
+ * content-description, or the class name for a widget.
+ *
+ * If no other search criteria is specified, and there is more
+ * than one matching widget, the first widget in the tree
+ * is selected.
+ *
+ * @param val Value to match
+ * @return UiSelector with the specified search criteria
+ * @since API Level 18
+ */
+ public UiSelector checkable(boolean val) {
+ return buildSelector(SELECTOR_CHECKABLE, val);
+ }
+
+ /**
* Set the search criteria to match widgets that are long-clickable.
*
* Typically, using this search criteria alone is not useful.
@@ -638,6 +670,11 @@
return false;
}
break;
+ case UiSelector.SELECTOR_CHECKABLE:
+ if (node.isCheckable() != getBoolean(criterion)) {
+ return false;
+ }
+ break;
case UiSelector.SELECTOR_LONG_CLICKABLE:
if (node.isLongClickable() != getBoolean(criterion)) {
return false;
@@ -734,6 +771,12 @@
return false;
}
break;
+ case UiSelector.SELECTOR_RESOURCE_ID:
+ s = node.getViewIdResourceName();
+ if (s == null || !s.toString().contentEquals(getString(criterion))) {
+ return false;
+ }
+ break;
}
}
return matchOrUpdateInstance();
@@ -896,6 +939,9 @@
case SELECTOR_CLICKABLE:
builder.append("CLICKABLE=").append(mSelectorAttributes.valueAt(i));
break;
+ case SELECTOR_CHECKABLE:
+ builder.append("CHECKABLE=").append(mSelectorAttributes.valueAt(i));
+ break;
case SELECTOR_LONG_CLICKABLE:
builder.append("LONG_CLICKABLE=").append(mSelectorAttributes.valueAt(i));
break;
@@ -941,6 +987,9 @@
case SELECTOR_PACKAGE_NAME_REGEX:
builder.append("PACKAGE_NAME_REGEX=").append(mSelectorAttributes.valueAt(i));
break;
+ case SELECTOR_RESOURCE_ID:
+ builder.append("RESOURCE_ID=").append(mSelectorAttributes.valueAt(i));
+ break;
default:
builder.append("UNDEFINED="+criterion+" ").append(mSelectorAttributes.valueAt(i));
}
diff --git a/uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/core/UiWatcher.java
rename to uiautomator/library/core-src/com/android/uiautomator/core/UiWatcher.java
diff --git a/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java
deleted file mode 100644
index c97d5de..0000000
--- a/uiautomator/library/src/com/android/uiautomator/core/InteractionController.java
+++ /dev/null
@@ -1,661 +0,0 @@
-/*
- * 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.os.UserHandle;
-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 = Log.isLoggable(LOG_TAG, Log.DEBUG);
-
- 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 static final long REGULAR_CLICK_LENGTH = 100;
-
- 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, UserHandle.USER_OWNER, 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;
- }
-
- /**
- * Click at coordinates and blocks until the first specified accessibility event.
- *
- * All clicks will cause some UI change to occur. If the device is busy, this will
- * block until the device begins to process the click at which point the call returns
- * and normal wait for idle processing may begin. If no evens are detected for the
- * timeout period specified, the call will return anyway.
- * @param x
- * @param y
- * @param timeout
- * @param eventType is an {@link AccessibilityEvent} type
- * @return True if busy state is detected else false for timeout waiting for busy state
- */
- public boolean clickAndWaitForEvent(final int x, final int y, long timeout,
- final int eventType) {
- return clickAndWaitForEvents(x, y, timeout, false, eventType);
- }
-
- /**
- * Click at coordinates and blocks until the specified accessibility events. It is possible to
- * set the wait for all events to occur, in no specific order, or to the wait for any.
- *
- * @param x
- * @param y
- * @param timeout
- * @param waitForAll boolean to indicate whether to wait for any or all events
- * @param eventTypes mask
- * @return true if events are received, else false if timeout.
- */
- public boolean clickAndWaitForEvents(final int x, final int y, long timeout,
- boolean waitForAll, int eventTypes) {
- String logString = String.format("clickAndWaitForEvents(%d, %d, %d, %s, %d)", x, y, timeout,
- Boolean.toString(waitForAll), eventTypes);
- Log.d(LOG_TAG, logString);
-
- mUiAutomatorBridge.setOperationTime();
- Runnable command = new Runnable() {
- @Override
- public void run() {
- if(touchDown(x, y)) {
- SystemClock.sleep(REGULAR_CLICK_LENGTH);
- touchUp(x, y);
- }
- }
- };
- return runAndWaitForEvents(command, timeout, waitForAll, eventTypes) != null;
- }
-
- /**
- * Runs a command and waits for a specific accessibility event.
- * @param command is a Runnable to execute before waiting for the event.
- * @param timeout
- * @param eventType
- * @return The AccessibilityEvent if one is received, otherwise null.
- */
- private AccessibilityEvent runAndWaitForEvent(Runnable command, long timeout, int eventType) {
- return runAndWaitForEvents(command, timeout, false, eventType);
- }
-
- /**
- * Runs a command and waits for accessibility events. It is possible to set the wait for all
- * events to occur at least once for each, or wait for any one to occur at least once.
- *
- * @param command
- * @param timeout
- * @param waitForAll boolean to indicate whether to wait for any or all events
- * @param eventTypesMask
- * @return The AccessibilityEvent if one is received, otherwise null.
- */
- private AccessibilityEvent runAndWaitForEvents(Runnable command, long timeout,
- final boolean waitForAll, final int eventTypesMask) {
- if (eventTypesMask == 0)
- throw new IllegalArgumentException("events mask cannot be zero");
-
- class EventPredicate implements Predicate<AccessibilityEvent> {
- int mMask;
- EventPredicate(int mask) {
- mMask = mask;
- }
- @Override
- public boolean apply(AccessibilityEvent t) {
- // check current event in the list
- if ((t.getEventType() & mMask) != 0) {
- if (!waitForAll)
- return true;
-
- // remove from mask since this condition is satisfied
- mMask &= ~t.getEventType();
-
- // Since we're waiting for all events to be matched at least once
- if (mMask != 0)
- return false;
-
- // all matched
- return true;
- }
- // not one of our events
- return false;
- }
- }
-
- AccessibilityEvent event = null;
- try {
- event = mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command,
- new EventPredicate(eventTypesMask), timeout);
- } catch (TimeoutException e) {
- Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events: " + eventTypesMask);
- return null;
- } catch (Exception e) {
- Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
- return null;
- }
- return event;
- }
-
- /**
- * Send keys and blocks until the first specified accessibility event.
- *
- * Most key presses will cause some UI change to occur. If the device is busy, this will
- * block until the device begins to process the key press at which point the call returns
- * and normal wait for idle processing may begin. If no events are detected for the
- * timeout period specified, the call will return anyway with false.
- *
- * @param keyCode
- * @param metaState
- * @param eventType
- * @param timeout
- * @return true if events is received, otherwise false.
- */
- public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
- final int eventType, long timeout) {
- mUiAutomatorBridge.setOperationTime();
- Runnable command = new Runnable() {
- @Override
- public void run() {
- 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);
- injectEventSync(upEvent);
- }
- }
- };
-
- return runAndWaitForEvent(command, timeout, eventType) != null;
- }
-
- /**
- * Clicks at coordinates without waiting for device idle. This may be used for operations
- * that require stressing the target.
- * @param x
- * @param y
- * @return true if the click executed successfully
- */
- public boolean click(int x, int y) {
- Log.d(LOG_TAG, "click (" + x + ", " + y + ")");
- mUiAutomatorBridge.setOperationTime();
-
- if (touchDown(x, y)) {
- SystemClock.sleep(REGULAR_CLICK_LENGTH);
- if (touchUp(x, y))
- return true;
- }
- return false;
- }
-
- /**
- * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
- * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
- * no further waits will be performed and the function returns.
- * @param x
- * @param y
- * @param timeout
- * @return true if both events occurred in the expected order
- */
- public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
- return (clickAndWaitForEvents(x, y, timeout, true,
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED +
- AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED));
- }
-
- 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, 1);
- event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- return injectEventSync(event);
- }
-
- private boolean touchUp(int x, int y) {
- if (DEBUG) {
- Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
- }
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent event = MotionEvent.obtain(
- mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
- event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- mDownTime = 0;
- return injectEventSync(event);
- }
-
- private boolean touchMove(int x, int y) {
- if (DEBUG) {
- Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
- }
- final long eventTime = SystemClock.uptimeMillis();
- MotionEvent event = MotionEvent.obtain(
- mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
- event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
- return injectEventSync(event);
- }
-
- /**
- * Handle swipes in any direction where the result is a scroll event. This call blocks
- * until the UI has fired a scroll event or timeout.
- * @param downX
- * @param downY
- * @param upX
- * @param upY
- * @param steps
- * @return true if we are not at the beginning or end of the scrollable view.
- */
- public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
- final int steps) {
- Log.d(LOG_TAG, "scrollSwipe (" + downX + ", " + downY + ", " + upX + ", "
- + upY + ", " + steps +")");
-
- Runnable command = new Runnable() {
- @Override
- public void run() {
- swipe(downX, downY, upX, upY, steps);
- }
- };
-
- AccessibilityEvent event = runAndWaitForEvent(command,
- DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS, AccessibilityEvent.TYPE_VIEW_SCROLLED);
- if (event == null) {
- return false;
- }
- // AdapterViews have indices we can use to check for the beginning.
- if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
- boolean foundEnd = event.getFromIndex() == 0 ||
- (event.getItemCount() - 1) == event.getToIndex();
- Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
- } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
- // Determine if we are scrolling vertically or horizontally.
- if (downX == upX) {
- // Vertical
- boolean foundEnd = event.getScrollY() == 0 ||
- event.getScrollY() == event.getMaxScrollY();
- Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
- } else if (downY == upY) {
- // Horizontal
- boolean foundEnd = event.getScrollX() == 0 ||
- event.getScrollX() == event.getMaxScrollX();
- Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
- }
- }
- return event != null;
- }
-
- /**
- * Handle swipes in any direction.
- * @param downX
- * @param downY
- * @param upX
- * @param upY
- * @param steps
- * @return true if the swipe executed successfully
- */
- public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
- 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/UiAutomatorBridge.java b/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java
deleted file mode 100644
index 90aa4df..0000000
--- a/uiautomator/library/src/com/android/uiautomator/core/UiAutomatorBridge.java
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java b/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java
new file mode 100644
index 0000000..1afa513
--- /dev/null
+++ b/uiautomator/library/testrunner-src/com/android/uiautomator/core/ShellUiAutomatorBridge.java
@@ -0,0 +1,122 @@
+/*
+ * 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.app.UiAutomation;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.database.Cursor;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.IPowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Display;
+import android.view.IWindowManager;
+
+/**
+ * @hide
+ */
+public class ShellUiAutomatorBridge extends UiAutomatorBridge {
+
+ private static final String LOG_TAG = ShellUiAutomatorBridge.class.getSimpleName();
+
+ public ShellUiAutomatorBridge(UiAutomation uiAutomation) {
+ super(uiAutomation);
+ }
+
+ public Display getDefaultDisplay() {
+ return DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
+ }
+
+ public 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, UserHandle.USER_OWNER, token);
+ if (holder == null) {
+ throw new IllegalStateException("Could not find provider: " + providerName);
+ }
+ provider = holder.provider;
+ cursor = provider.query(null, 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;
+ }
+
+ @Override
+ public int getRotation() {
+ IWindowManager wm =
+ IWindowManager.Stub.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
+ int ret = -1;
+ try {
+ ret = wm.getRotation();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error getting screen rotation", e);
+ throw new RuntimeException(e);
+ }
+ return ret;
+ }
+
+ @Override
+ public boolean isScreenOn() {
+ IPowerManager pm =
+ IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ boolean ret = false;
+ try {
+ ret = pm.isScreenOn();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Error getting screen status", e);
+ throw new RuntimeException(e);
+ }
+ return ret;
+ }
+}
diff --git a/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java b/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java
new file mode 100644
index 0000000..bb97f36
--- /dev/null
+++ b/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java
@@ -0,0 +1,39 @@
+package com.android.uiautomator.core;
+
+import android.app.UiAutomation;
+import android.app.UiAutomationConnection;
+import android.os.HandlerThread;
+
+/**
+ * @hide
+ */
+public class UiAutomationShellWrapper {
+
+ private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
+
+ private final HandlerThread mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
+
+ private UiAutomation mUiAutomation;
+
+ public void connect() {
+ if (mHandlerThread.isAlive()) {
+ throw new IllegalStateException("Already connected!");
+ }
+ mHandlerThread.start();
+ mUiAutomation = new UiAutomation(mHandlerThread.getLooper(),
+ new UiAutomationConnection());
+ mUiAutomation.connect();
+ }
+
+ public void disconnect() {
+ if (!mHandlerThread.isAlive()) {
+ throw new IllegalStateException("Already disconnected!");
+ }
+ mUiAutomation.disconnect();
+ mHandlerThread.quit();
+ }
+
+ public UiAutomation getUiAutomation() {
+ return mUiAutomation;
+ }
+}
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/testrunner/IAutomationSupport.java
rename to uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/IAutomationSupport.java
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java
similarity index 97%
rename from uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java
rename to uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java
index 9fc0b0b..cda49f6 100644
--- a/uiautomator/library/src/com/android/uiautomator/testrunner/TestCaseCollector.java
+++ b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/TestCaseCollector.java
@@ -16,10 +16,7 @@
package com.android.uiautomator.testrunner;
-import junit.framework.AssertionFailedError;
-import junit.framework.Test;
import junit.framework.TestCase;
-import junit.framework.TestResult;
import java.lang.reflect.Method;
import java.util.ArrayList;
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
rename to uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java
similarity index 100%
rename from uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java
rename to uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestCaseFilter.java
diff --git a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
similarity index 95%
rename from uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
rename to uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
index 4f41a5c..1b751da 100644
--- a/uiautomator/library/src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
+++ b/uiautomator/library/testrunner-src/com/android/uiautomator/testrunner/UiAutomatorTestRunner.java
@@ -22,12 +22,15 @@
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Debug;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.SystemClock;
import android.test.RepetitiveTest;
import android.util.Log;
+import com.android.uiautomator.core.ShellUiAutomatorBridge;
import com.android.uiautomator.core.Tracer;
+import com.android.uiautomator.core.UiAutomationShellWrapper;
import com.android.uiautomator.core.UiDevice;
import junit.framework.AssertionFailedError;
@@ -54,6 +57,8 @@
private static final int EXIT_OK = 0;
private static final int EXIT_EXCEPTION = -1;
+ private static final String HANDLER_THREAD_NAME = "UiAutomatorHandlerThread";
+
private boolean mDebug;
private Bundle mParams = null;
private UiDevice mUiDevice;
@@ -67,6 +72,8 @@
};
private List<TestListener> mTestListeners = new ArrayList<TestListener>();
+ private HandlerThread mHandlerThread;
+
public void run(List<String> testClasses, Bundle params, boolean debug) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
@@ -102,7 +109,13 @@
if (mDebug) {
Debug.waitForDebugger();
}
+ mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME);
+ mHandlerThread.setDaemon(true);
+ mHandlerThread.start();
+ UiAutomationShellWrapper automationWrapper = new UiAutomationShellWrapper();
+ automationWrapper.connect();
mUiDevice = UiDevice.getInstance();
+ mUiDevice.initialize(new ShellUiAutomatorBridge(automationWrapper.getUiAutomation()));
List<TestCase> testCases = collector.getTestCases();
Bundle testRunOutput = new Bundle();
@@ -149,6 +162,8 @@
} finally {
long runTime = SystemClock.uptimeMillis() - startTime;
resultPrinter.print(testRunResult, runTime, testRunOutput);
+ automationWrapper.disconnect();
+ mHandlerThread.quit();
}
}