--- /dev/null
+#
+# Copyright (C) 2009 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.
+#
+MONKEYRUNNER_LOCAL_DIR := $(call my-dir)
+include $(MONKEYRUNNER_LOCAL_DIR)/etc/Android.mk
+include $(MONKEYRUNNER_LOCAL_DIR)/src/Android.mk
+include $(MONKEYRUNNER_LOCAL_DIR)/test/Android.mk
--- /dev/null
+
+ Copyright (c) 2005-2008, 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.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
--- /dev/null
+#
+# Copyright (C) 2009 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_PREBUILT_EXECUTABLES := monkeyrunner
+include $(BUILD_HOST_PREBUILT)
--- /dev/null
+Main-Class: com.android.monkeyrunner.MonkeyRunnerStarter
--- /dev/null
+#!/bin/sh
+# Copyright 2005-2007, 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+ newProg=`/bin/ls -ld "${prog}"`
+ newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+ if expr "x${newProg}" : 'x/' >/dev/null; then
+ prog="${newProg}"
+ else
+ progdir=`dirname "${prog}"`
+ prog="${progdir}/${newProg}"
+ fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=monkeyrunner.jar
+frameworkdir="$progdir"
+libdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/tools/lib
+ libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/framework
+ libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ echo `basename "$prog"`": can't find $jarfile"
+ exit 1
+fi
+
+
+# Check args.
+if [ debug = "$1" ]; then
+ # add this in for debugging
+ java_debug=-agentlib:jdwp=transport=dt_socket,server=y,address=8050,suspend=y
+ shift 1
+else
+ java_debug=
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+ jarpath=`cygpath -w "$frameworkdir/$jarfile"`
+ progdir=`cygpath -w "$progdir"`
+else
+ jarpath="$frameworkdir/$jarfile"
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@"
--- /dev/null
+#!/usr/bin/python2.4
+#
+# Copyright 2010, 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.
+
+"""Test cases for com.android.monkeyrunner.MonkeyRunner."""
+
+import time
+import unittest
+
+from com.android.monkeyrunner import MonkeyRunner
+
+
+class TestMonkeyRunnerArgParsing(unittest.TestCase):
+ """Test ArgParsing for the MonkeyRunner methods."""
+ def testWaitForConnectionNoArgs(self):
+ MonkeyRunner.waitForConnection()
+
+ def testWaitForConnectionSingleArg(self):
+ MonkeyRunner.waitForConnection(2)
+
+ def testWaitForConnectionDoubleArg(self):
+ MonkeyRunner.waitForConnection(2, '*')
+
+ def testWaitForConnectionKeywordArg(self):
+ MonkeyRunner.waitForConnection(timeout=2, deviceId='foo')
+
+ def testWaitForConnectionKeywordArgTooMany(self):
+ try:
+ MonkeyRunner.waitForConnection(timeout=2, deviceId='foo', extra='fail')
+ except TypeError:
+ return
+ self.fail('Should have raised TypeError')
+
+ def testSleep(self):
+ start = time.time()
+ MonkeyRunner.sleep(1.5)
+ end = time.time()
+
+ self.assertTrue(end - start >= 1.5)
+
+if __name__ == '__main__':
+ unittest.main()
--- /dev/null
+#!/usr/bin/python2.4
+#
+# Copyright 2010, 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.
+
+"""Test runner to run all the tests in this package."""
+
+import os
+import re
+import sys
+import unittest
+
+
+TESTCASE_RE = re.compile('_test\.py$')
+
+
+def AllTestFilesInDir(path):
+ """Finds all the unit test files in the given path."""
+ return filter(TESTCASE_RE.search, os.listdir(path))
+
+
+def suite(loader=unittest.defaultTestLoader):
+ """Creates the all_tests TestSuite."""
+ script_parent_path = os.path.abspath(os.path.dirname(sys.argv[0]))
+ # Find all the _test.py files in the same directory we are in
+ test_files = AllTestFilesInDir(script_parent_path)
+ # Convert them into module names
+ module_names = [os.path.splitext(f)[0] for f in test_files]
+ # And import them
+ modules = map(__import__, module_names)
+ # And create the test suite for all these modules
+ return unittest.TestSuite([loader.loadTestsFromModule(m) for m in modules])
+
+if __name__ == '__main__':
+ result = unittest.TextTestRunner().run(suite())
+ if not result.wasSuccessful():
+ # On failure return an error code
+ sys.exit(1)
--- /dev/null
+#!/usr/bin/env monkeyrunner
+# Copyright 2010, 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.
+from com.android.monkeyrunner import MonkeyRunner as mr
+
+import os
+import sys
+
+supported_formats = ['html', 'text']
+
+if len(sys.argv) != 3:
+ print 'help.py: format output'
+ sys.exit(1)
+
+(format, saveto_path) = sys.argv[1:]
+
+if not format.lower() in supported_formats:
+ print 'format %s is not a supported format' % format
+ sys.exit(2)
+
+output = mr.help(format=format)
+if not output:
+ print 'Error generating help format'
+ sys.exit(3)
+
+dirname = os.path.dirname(saveto_path)
+try:
+ os.makedirs(dirname)
+except:
+ print 'oops'
+ pass # It already existed
+
+fp = open(saveto_path, 'w')
+fp.write(output)
+fp.close()
+
+sys.exit(0)
--- /dev/null
+#
+# Copyright (C) 2009 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-subdir-java-files)
+
+LOCAL_JAR_MANIFEST := ../etc/manifest.txt
+LOCAL_JAVA_LIBRARIES := \
+ ddmlib \
+ jython \
+ guavalib \
+ clearsilver
+LOCAL_SHARED_LIBRARIES := libclearsilver-jni
+LOCAL_JAVA_RESOURCE_DIRS := resources
+
+LOCAL_MODULE := monkeyrunner
+
+include $(BUILD_HOST_JAVA_LIBRARY)
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.ImmutableMap.Builder;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.python.core.ArgParser;
+import org.python.core.Py;
+import org.python.core.PyDictionary;
+import org.python.core.PyFloat;
+import org.python.core.PyInteger;
+import org.python.core.PyList;
+import org.python.core.PyNone;
+import org.python.core.PyObject;
+import org.python.core.PyString;
+import org.python.core.PyTuple;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Collection of useful utilities function for interacting with the Jython interpreter.
+ */
+public final class JythonUtils {
+ private static final Logger LOG = Logger.getLogger(JythonUtils.class.getCanonicalName());
+ private JythonUtils() { }
+
+ /**
+ * Mapping of PyObject classes to the java class we want to convert them to.
+ */
+ private static final Map<Class<? extends PyObject>, Class<?>> PYOBJECT_TO_JAVA_OBJECT_MAP;
+ static {
+ Builder<Class<? extends PyObject>, Class<?>> builder = ImmutableMap.builder();
+
+ builder.put(PyString.class, String.class);
+ // What python calls float, most people call double
+ builder.put(PyFloat.class, Double.class);
+ builder.put(PyInteger.class, Integer.class);
+
+ PYOBJECT_TO_JAVA_OBJECT_MAP = builder.build();
+ }
+
+ /**
+ * Utility method to be called from Jython bindings to give proper handling of keyword and
+ * positional arguments.
+ *
+ * @param args the PyObject arguments from the binding
+ * @param kws the keyword arguments from the binding
+ * @return an ArgParser for this binding, or null on error
+ */
+ public static ArgParser createArgParser(PyObject[] args, String[] kws) {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ // Up 2 levels in the current stack to give us the calling function
+ StackTraceElement element = stackTrace[2];
+
+ String methodName = element.getMethodName();
+ String className = element.getClassName();
+
+ Class<?> clz;
+ try {
+ clz = Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ LOG.log(Level.SEVERE, "Got exception: ", e);
+ return null;
+ }
+
+ Method m;
+
+ try {
+ m = clz.getMethod(methodName, PyObject[].class, String[].class);
+ } catch (SecurityException e) {
+ LOG.log(Level.SEVERE, "Got exception: ", e);
+ return null;
+ } catch (NoSuchMethodException e) {
+ LOG.log(Level.SEVERE, "Got exception: ", e);
+ return null;
+ }
+
+ MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class);
+ return new ArgParser(methodName, args, kws,
+ annotation.args());
+ }
+
+ /**
+ * Get a python floating point value from an ArgParser.
+ *
+ * @param ap the ArgParser to get the value from.
+ * @param position the position in the parser
+ * @return the double value
+ */
+ public static double getFloat(ArgParser ap, int position) {
+ PyObject arg = ap.getPyObject(position);
+
+ if (Py.isInstance(arg, PyFloat.TYPE)) {
+ return ((PyFloat) arg).asDouble();
+ }
+ if (Py.isInstance(arg, PyInteger.TYPE)) {
+ return ((PyInteger) arg).asDouble();
+ }
+ throw Py.TypeError("Unable to parse argument: " + position);
+ }
+
+ /**
+ * Get a python floating point value from an ArgParser.
+ *
+ * @param ap the ArgParser to get the value from.
+ * @param position the position in the parser
+ * @param defaultValue the default value to return if the arg isn't specified.
+ * @return the double value
+ */
+ public static double getFloat(ArgParser ap, int position, double defaultValue) {
+ PyObject arg = ap.getPyObject(position, new PyFloat(defaultValue));
+
+ if (Py.isInstance(arg, PyFloat.TYPE)) {
+ return ((PyFloat) arg).asDouble();
+ }
+ if (Py.isInstance(arg, PyInteger.TYPE)) {
+ return ((PyInteger) arg).asDouble();
+ }
+ throw Py.TypeError("Unable to parse argument: " + position);
+ }
+
+ /**
+ * Get a list of arguments from an ArgParser.
+ *
+ * @param ap the ArgParser
+ * @param position the position in the parser to get the argument from
+ * @return a list of those items
+ */
+ @SuppressWarnings("unchecked")
+ public static List<Object> getList(ArgParser ap, int position) {
+ PyObject arg = ap.getPyObject(position, Py.None);
+ if (Py.isInstance(arg, PyNone.TYPE)) {
+ return Collections.emptyList();
+ }
+
+ List<Object> ret = Lists.newArrayList();
+ PyList array = (PyList) arg;
+ for (int x = 0; x < array.__len__(); x++) {
+ PyObject item = array.__getitem__(x);
+
+ Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(item.getClass());
+ if (javaClass != null) {
+ ret.add(item.__tojava__(javaClass));
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Get a dictionary from an ArgParser. For ease of use, key types are always coerced to
+ * strings. If key type cannot be coeraced to string, an exception is raised.
+ *
+ * @param ap the ArgParser to work with
+ * @param position the position in the parser to get.
+ * @return a Map mapping the String key to the value
+ */
+ public static Map<String, Object> getMap(ArgParser ap, int position) {
+ PyObject arg = ap.getPyObject(position, Py.None);
+ if (Py.isInstance(arg, PyNone.TYPE)) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, Object> ret = Maps.newHashMap();
+ // cast is safe as getPyObjectbyType ensures it
+ PyDictionary dict = (PyDictionary) arg;
+ PyList items = dict.items();
+ for (int x = 0; x < items.__len__(); x++) {
+ // It's a list of tuples
+ PyTuple item = (PyTuple) items.__getitem__(x);
+ // We call str(key) on the key to get the string and then convert it to the java string.
+ String key = (String) item.__getitem__(0).__str__().__tojava__(String.class);
+ PyObject value = item.__getitem__(1);
+
+ // Look up the conversion type and convert the value
+ Class<?> javaClass = PYOBJECT_TO_JAVA_OBJECT_MAP.get(value.getClass());
+ if (javaClass != null) {
+ ret.put(key, value.__tojava__(javaClass));
+ }
+ }
+ return ret;
+ }
+
+ private static PyObject convertObject(Object o) {
+ if (o instanceof String) {
+ return new PyString((String) o);
+ } else if (o instanceof Double) {
+ return new PyFloat((Double) o);
+ } else if (o instanceof Integer) {
+ return new PyInteger((Integer) o);
+ } else if (o instanceof Float) {
+ float f = (Float) o;
+ return new PyFloat(f);
+ }
+ return Py.None;
+ }
+
+ /**
+ * Convert the given Java Map into a PyDictionary.
+ *
+ * @param map the map to convert
+ * @return the python dictionary
+ */
+ public static PyDictionary convertMapToDict(Map<String, Object> map) {
+ Map<PyObject, PyObject> resultMap = Maps.newHashMap();
+
+ for (Entry<String, Object> entry : map.entrySet()) {
+ resultMap.put(new PyString(entry.getKey()),
+ convertObject(entry.getValue()));
+ }
+ return new PyDictionary(resultMap);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.python.core.ArgParser;
+import org.python.core.Py;
+import org.python.core.PyDictionary;
+import org.python.core.PyException;
+import org.python.core.PyObject;
+import org.python.core.PyTuple;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/*
+ * Abstract base class that represents a single connected Android
+ * Device and provides MonkeyRunner API methods for interacting with
+ * that device. Each backend will need to create a concrete
+ * implementation of this class.
+ */
+public abstract class MonkeyDevice {
+ /**
+ * Create a MonkeyMananger for talking to this device.
+ *
+ * NOTE: This is not part of the jython API.
+ *
+ * @return the MonkeyManager
+ */
+ public abstract MonkeyManager getManager();
+
+ /**
+ * Dispose of any native resoureces this device may have taken hold of.
+ *
+ * NOTE: This is not part of the jython API.
+ */
+ public abstract void dispose();
+
+ @MonkeyRunnerExported(doc = "Fetch the screenbuffer from the device and return it.",
+ returns = "The captured snapshot.")
+ public abstract MonkeyImage takeSnapshot();
+
+ @MonkeyRunnerExported(doc = "Get a MonkeyRunner property (like build.fingerprint)",
+ args = {"key"},
+ argDocs = {"The key of the property to return"},
+ returns = "The value of the property")
+ public String getProperty(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ return getProperty(ap.getString(0));
+ }
+
+ @MonkeyRunnerExported(doc = "Get a system property (returns the same value as getprop).",
+ args = {"key"},
+ argDocs = {"The key of the property to return"},
+ returns = "The value of the property")
+ public String getSystemProperty(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+ return getSystemProperty(ap.getString(0));
+ }
+
+ @MonkeyRunnerExported(doc = "Enumeration of possible touch and press event types. This gets " +
+ "passed into a press or touch call to specify the event type.",
+ argDocs = {"Indicates the down part of a touch/press event",
+ "Indicates the up part of a touch/press event.",
+ "Indicates that the monkey should send a down event immediately " +
+ "followed by an up event"})
+ public enum TouchPressType {
+ DOWN, UP, DOWN_AND_UP
+ }
+
+ @MonkeyRunnerExported(doc = "Send a touch event at the specified location",
+ args = { "x", "y", "type" },
+ argDocs = { "x coordinate", "y coordinate", "the type of touch event to send"})
+ public void touch(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ int x = ap.getInt(0);
+ int y = ap.getInt(1);
+
+ // Default
+ MonkeyDevice.TouchPressType type = MonkeyDevice.TouchPressType.DOWN_AND_UP;
+ try {
+ PyObject pyObject = ap.getPyObject(2);
+ type = (TouchPressType) pyObject.__tojava__(MonkeyDevice.TouchPressType.class);
+ } catch (PyException e) {
+ // bad stuff was passed in, just use the already specified default value
+ type = MonkeyDevice.TouchPressType.DOWN_AND_UP;
+ }
+ touch(x, y, type);
+ }
+
+ @MonkeyRunnerExported(doc = "Simulate a drag on the screen.",
+ args = { "start", "end", "duration", "steps"},
+ argDocs = { "The starting point for the drag (a tuple of x,y)",
+ "The end point for the drag (a tuple of x,y)",
+ "How long (in seconds) should the drag take (default is 1.0 seconds)",
+ "The number of steps to take when interpolating points. (default is 10)"})
+ public void drag(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ PyObject startObject = ap.getPyObject(0);
+ if (!(startObject instanceof PyTuple)) {
+ throw Py.TypeError("Agrument 0 is not a tuple");
+ }
+ PyObject endObject = ap.getPyObject(1);
+ if (!(endObject instanceof PyTuple)) {
+ throw Py.TypeError("Agrument 1 is not a tuple");
+ }
+
+ PyTuple start = (PyTuple) startObject;
+ PyTuple end = (PyTuple) endObject;
+
+ int startx = (Integer) start.__getitem__(0).__tojava__(Integer.class);
+ int starty = (Integer) start.__getitem__(1).__tojava__(Integer.class);
+ int endx = (Integer) end.__getitem__(0).__tojava__(Integer.class);
+ int endy = (Integer) end.__getitem__(1).__tojava__(Integer.class);
+
+ double seconds = JythonUtils.getFloat(ap, 2, 1.0);
+ long ms = (long) (seconds * 1000.0);
+
+ int steps = ap.getInt(3, 10);
+
+ drag(startx, starty, endx, endy, steps, ms);
+ }
+
+ @MonkeyRunnerExported(doc = "Send a key press event to the specified button",
+ args = { "name", "type" },
+ argDocs = { "the name of the key to press", "the type of touch event to send"})
+ public void press(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String name = ap.getString(0);
+
+ // Default
+ MonkeyDevice.TouchPressType type = MonkeyDevice.TouchPressType.DOWN_AND_UP;
+ try {
+ PyObject pyObject = ap.getPyObject(1);
+ type = (TouchPressType) pyObject.__tojava__(MonkeyDevice.TouchPressType.class);
+ } catch (PyException e) {
+ // bad stuff was passed in, just use the already specified default value
+ type = MonkeyDevice.TouchPressType.DOWN_AND_UP;
+ }
+ press(name, type);
+ }
+
+ @MonkeyRunnerExported(doc = "Type the specified string on the keyboard.",
+ args = { "message" },
+ argDocs = { "the message to type." })
+ public void type(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String message = ap.getString(0);
+ type(message);
+ }
+
+ @MonkeyRunnerExported(doc = "Execute the given command on the shell.",
+ args = { "cmd"},
+ argDocs = { "The command to execute" },
+ returns = "The output of the command")
+ public String shell(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String cmd = ap.getString(0);
+ return shell(cmd);
+ }
+
+ @MonkeyRunnerExported(doc = "Reboot the specified device",
+ args = { "into" },
+ argDocs = { "the bootloader to reboot into (bootloader, recovery, or None)"})
+ public void reboot(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String into = ap.getString(0, null);
+
+ reboot(into);
+ }
+
+ @MonkeyRunnerExported(doc = "Install the specified apk onto the device.",
+ args = { "path" },
+ argDocs = { "The path on the host filesystem to the APK to install." },
+ returns = "True if install succeeded")
+ public boolean installPackage(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String path = ap.getString(0);
+ return installPackage(path);
+ }
+
+ @MonkeyRunnerExported(doc = "Remove the specified package from the device.",
+ args = { "package"},
+ argDocs = { "The name of the package to uninstall"},
+ returns = "'True if remove succeeded")
+ public boolean removePackage(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String packageName = ap.getString(0);
+ return removePackage(packageName);
+ }
+
+ @MonkeyRunnerExported(doc = "Start the Activity specified by the intent.",
+ args = { "uri", "action", "data", "mimetype", "categories", "extras",
+ "component", "flags" },
+ argDocs = { "The URI for the intent",
+ "The action for the intent",
+ "The data URI for the intent",
+ "The mime type for the intent",
+ "The list of category names for the intent",
+ "A dictionary of extras to add to the intent. Types of these extras " +
+ "are inferred from the python types of the values",
+ "The component of the intent",
+ "A list of flags for the intent" })
+ public void startActivity(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String uri = ap.getString(0, null);
+ String action = ap.getString(1, null);
+ String data = ap.getString(2, null);
+ String mimetype = ap.getString(3, null);
+ Collection<String> categories = Collections2.transform(JythonUtils.getList(ap, 4),
+ Functions.toStringFunction());
+ Map<String, Object> extras = JythonUtils.getMap(ap, 5);
+ String component = ap.getString(6, null);
+ int flags = ap.getInt(7, 0);
+
+ startActivity(uri, action, data, mimetype, categories, extras, component, flags);
+ }
+
+ @MonkeyRunnerExported(doc = "Start the specified broadcast intent on the device.",
+ args = { "uri", "action", "data", "mimetype", "categories", "extras",
+ "component", "flags" },
+ argDocs = { "The URI for the intent",
+ "The action for the intent",
+ "The data URI for the intent",
+ "The mime type for the intent",
+ "The list of category names for the intent",
+ "A dictionary of extras to add to the intent. Types of these extras " +
+ "are inferred from the python types of the values",
+ "The component of the intent",
+ "A list of flags for the intent" })
+ public void broadcastIntent(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String uri = ap.getString(0, null);
+ String action = ap.getString(1, null);
+ String data = ap.getString(2, null);
+ String mimetype = ap.getString(3, null);
+ Collection<String> categories = Collections2.transform(JythonUtils.getList(ap, 4),
+ Functions.toStringFunction());
+ Map<String, Object> extras = JythonUtils.getMap(ap, 5);
+ String component = ap.getString(6, null);
+ int flags = ap.getInt(7, 0);
+
+ broadcastIntent(uri, action, data, mimetype, categories, extras, component, flags);
+ }
+
+ @MonkeyRunnerExported(doc = "Instrument the specified package and return the results from it.",
+ args = { "className", "args" },
+ argDocs = { "The class name to instrument (like com.android.test/.TestInstrument)",
+ "A Map of String to Objects for the aruments to pass to this " +
+ "instrumentation (default value is None)" },
+ returns = "A map of string to objects for the results this instrumentation returned")
+ public PyDictionary instrument(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String packageName = ap.getString(0);
+ Map<String, Object> instrumentArgs = JythonUtils.getMap(ap, 1);
+ if (instrumentArgs == null) {
+ instrumentArgs = Collections.emptyMap();
+ }
+
+ Map<String, Object> result = instrument(packageName, instrumentArgs);
+ return JythonUtils.convertMapToDict(result);
+ }
+
+ @MonkeyRunnerExported(doc = "Wake up the screen on the device")
+ public void wake(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ wake();
+ }
+
+ /**
+ * Reboot the device.
+ *
+ * @param into which bootloader to boot into. Null means default reboot.
+ */
+ protected abstract void reboot(@Nullable String into);
+
+ protected abstract String getProperty(String key);
+ protected abstract String getSystemProperty(String key);
+ protected abstract void touch(int x, int y, TouchPressType type);
+ protected abstract void press(String keyName, TouchPressType type);
+ protected abstract void drag(int startx, int starty, int endx, int endy, int steps, long ms);
+ protected abstract void type(String string);
+ protected abstract String shell(String cmd);
+ protected abstract boolean installPackage(String path);
+ protected abstract boolean removePackage(String packageName);
+ protected abstract void startActivity(@Nullable String uri, @Nullable String action,
+ @Nullable String data, @Nullable String mimetype,
+ Collection<String> categories, Map<String, Object> extras, @Nullable String component,
+ int flags);
+ protected abstract void broadcastIntent(@Nullable String uri, @Nullable String action,
+ @Nullable String data, @Nullable String mimetype,
+ Collection<String> categories, Map<String, Object> extras, @Nullable String component,
+ int flags);
+ protected abstract Map<String, Object> instrument(String packageName,
+ Map<String, Object> args);
+ protected abstract void wake();
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.collect.Maps;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.logging.Formatter;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/*
+ * Custom Logging Formatter for MonkeyRunner that generates all log
+ * messages on a single line.
+ */
+public class MonkeyFormatter extends Formatter {
+ public static final Formatter DEFAULT_INSTANCE = new MonkeyFormatter();
+
+ private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyMMdd HH:mm:ss.SSS");
+
+ private static Map<Level, String> LEVEL_TO_STRING_CACHE = Maps.newHashMap();
+
+ private static final String levelToString(Level level) {
+ String levelName = LEVEL_TO_STRING_CACHE.get(level);
+ if (levelName == null) {
+ levelName = level.getName().substring(0, 1);
+ LEVEL_TO_STRING_CACHE.put(level, levelName);
+ }
+ return levelName;
+ }
+
+ private static String getHeader(LogRecord record) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(FORMAT.format(new Date(record.getMillis()))).append(":");
+ sb.append(levelToString(record.getLevel())).append(" ");
+
+ sb.append("[").append(Thread.currentThread().getName()).append("] ");
+
+ String loggerName = record.getLoggerName();
+ if (loggerName != null) {
+ sb.append("[").append(loggerName).append("]");
+ }
+ return sb.toString();
+ }
+
+ private class PrintWriterWithHeader extends PrintWriter {
+ private final ByteArrayOutputStream out;
+ private final String header;
+
+ public PrintWriterWithHeader(String header) {
+ this(header, new ByteArrayOutputStream());
+ }
+
+ public PrintWriterWithHeader(String header, ByteArrayOutputStream out) {
+ super(out, true);
+ this.header = header;
+ this.out = out;
+ }
+
+ @Override
+ public void println(Object x) {
+ print(header);
+ super.println(x);
+ }
+
+ @Override
+ public void println(String x) {
+ print(header);
+ super.println(x);
+ }
+
+ @Override
+ public String toString() {
+ return out.toString();
+ }
+ }
+
+ @Override
+ public String format(LogRecord record) {
+ Throwable thrown = record.getThrown();
+ String header = getHeader(record);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(header);
+ sb.append(" ").append(formatMessage(record));
+ sb.append("\n");
+
+ // Print the exception here if we caught it
+ if (thrown != null) {
+
+ PrintWriter pw = new PrintWriterWithHeader(header);
+ thrown.printStackTrace(pw);
+ sb.append(pw.toString());
+ }
+
+ return sb.toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Preconditions;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.python.core.ArgParser;
+import org.python.core.PyInteger;
+import org.python.core.PyObject;
+import org.python.core.PyTuple;
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.Iterator;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+import javax.imageio.stream.ImageOutputStream;
+
+/**
+ * Jython object to encapsulate images that have been taken.
+ */
+public abstract class MonkeyImage {
+ /**
+ * Convert the MonkeyImage into a BufferedImage.
+ *
+ * @return a BufferedImage for this MonkeyImage.
+ */
+ public abstract BufferedImage createBufferedImage();
+
+ // Cache the BufferedImage so we don't have to generate it every time.
+ private WeakReference<BufferedImage> cachedBufferedImage = null;
+
+ /**
+ * Utility method to handle getting the BufferedImage and managing the cache.
+ *
+ * @return the BufferedImage for this image.
+ */
+ private BufferedImage getBufferedImage() {
+ // Check the cache first
+ if (cachedBufferedImage != null) {
+ BufferedImage img = cachedBufferedImage.get();
+ if (img != null) {
+ return img;
+ }
+ }
+
+ // Not in the cache, so create it and cache it.
+ BufferedImage img = createBufferedImage();
+ cachedBufferedImage = new WeakReference<BufferedImage>(img);
+ return img;
+ }
+
+ @MonkeyRunnerExported(doc = "Encode the image into a format and return the bytes.",
+ args = {"format"},
+ argDocs = { "The (optional) format in which to encode the image (PNG for example)" },
+ returns = "A String containing the bytes.")
+ public byte[] convertToBytes(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String format = ap.getString(0, "png");
+
+ BufferedImage argb = convertSnapshot();
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ ImageIO.write(argb, format, os);
+ } catch (IOException e) {
+ return new byte[0];
+ }
+ return os.toByteArray();
+ }
+
+ @MonkeyRunnerExported(doc = "Write out the file to the specified location. If no " +
+ "format is specified, this function tries to guess at the output format " +
+ "depending on the file extension given. If unable to determine, it uses PNG.",
+ args = {"path", "format"},
+ argDocs = {"Where to write out the file",
+ "The format in which to encode the image (PNG for example)"},
+ returns = "True if writing succeeded.")
+ public boolean writeToFile(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String path = ap.getString(0);
+ String format = ap.getString(1, null);
+
+ if (format != null) {
+ return writeToFile(path, format);
+ }
+ int offset = path.lastIndexOf('.');
+ if (offset < 0) {
+ return writeToFile(path, "png");
+ }
+ String ext = path.substring(offset + 1);
+ Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext);
+ if (!writers.hasNext()) {
+ return writeToFile(path, "png");
+ }
+ ImageWriter writer = writers.next();
+ BufferedImage image = getBufferedImage();
+ try {
+ File f = new File(path);
+ f.delete();
+
+ ImageOutputStream outputStream = ImageIO.createImageOutputStream(f);
+ writer.setOutput(outputStream);
+
+ try {
+ writer.write(image);
+ } finally {
+ writer.dispose();
+ outputStream.flush();
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @MonkeyRunnerExported(doc = "Get a single ARGB pixel from the image",
+ args = { "x", "y" },
+ argDocs = { "the x offset of the pixel", "the y offset of the pixel" },
+ returns = "A tuple of (A, R, G, B) for the pixel")
+ public PyObject getRawPixel(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ int x = ap.getInt(0);
+ int y = ap.getInt(1);
+ int pixel = getPixel(x, y);
+ PyInteger a = new PyInteger((pixel & 0xFF000000) >> 24);
+ PyInteger r = new PyInteger((pixel & 0x00FF0000) >> 16);
+ PyInteger g = new PyInteger((pixel & 0x0000FF00) >> 8);
+ PyInteger b = new PyInteger((pixel & 0x000000FF) >> 0);
+ return new PyTuple(a, r, g ,b);
+ }
+
+ @MonkeyRunnerExported(doc = "Get a single ARGB pixel from the image",
+ args = { "x", "y" },
+ argDocs = { "the x offset of the pixel", "the y offset of the pixel" },
+ returns = "An integer for the ARGB pixel")
+ public int getRawPixelInt(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ int x = ap.getInt(0);
+ int y = ap.getInt(1);
+ return getPixel(x, y);
+ }
+
+ private int getPixel(int x, int y) {
+ BufferedImage image = getBufferedImage();
+ return image.getRGB(x, y);
+ }
+
+ private BufferedImage convertSnapshot() {
+ BufferedImage image = getBufferedImage();
+
+ // Convert the image to ARGB so ImageIO writes it out nicely
+ BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_INT_ARGB);
+ Graphics g = argb.createGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ return argb;
+ }
+
+ public boolean writeToFile(String path, String format) {
+ BufferedImage argb = convertSnapshot();
+
+ try {
+ ImageIO.write(argb, format, new File(path));
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ @MonkeyRunnerExported(doc = "Compare this image to the other image.",
+ args = {"other", "percent"},
+ argDocs = {"The other image.",
+ "A float from 0.0 to 1.0 indicating the percentage " +
+ "of pixels that need to be the same. Defaults to 1.0"},
+ returns = "True if they are the same image.")
+ public boolean sameAs(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ PyObject otherObject = ap.getPyObject(0);
+ MonkeyImage other = (MonkeyImage) otherObject.__tojava__(MonkeyImage.class);
+
+ double percent = JythonUtils.getFloat(ap, 1, 1.0);
+
+ BufferedImage otherImage = other.getBufferedImage();
+ BufferedImage myImage = getBufferedImage();
+
+ // Easy size check
+ if (otherImage.getWidth() != myImage.getWidth()) {
+ return false;
+ }
+ if (otherImage.getHeight() != myImage.getHeight()) {
+ return false;
+ }
+
+ int[] otherPixel = new int[1];
+ int[] myPixel = new int[1];
+
+ int width = myImage.getWidth();
+ int height = myImage.getHeight();
+
+ int numDiffPixels = 0;
+ // Now, go through pixel-by-pixel and check that the images are the same;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) {
+ numDiffPixels++;
+ }
+ }
+ }
+ double numberPixels = (height * width);
+ double diffPercent = numDiffPixels / numberPixels;
+ return percent <= 1.0 - diffPercent;
+ }
+
+ private static class BufferedImageMonkeyImage extends MonkeyImage {
+ private final BufferedImage image;
+
+ public BufferedImageMonkeyImage(BufferedImage image) {
+ this.image = image;
+ }
+
+ @Override
+ public BufferedImage createBufferedImage() {
+ return image;
+ }
+
+ }
+
+ @MonkeyRunnerExported(doc = "Get a sub-image of this image.",
+ args = {"rect"},
+ argDocs = {"A Tuple of (x, y, w, h) representing the area of the image to extract."},
+ returns = "The newly extracted image.")
+ public MonkeyImage getSubImage(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ PyTuple rect = (PyTuple) ap.getPyObjectByType(0, PyTuple.TYPE);
+ int x = rect.__getitem__(0).asInt();
+ int y = rect.__getitem__(1).asInt();
+ int w = rect.__getitem__(2).asInt();
+ int h = rect.__getitem__(3).asInt();
+
+ BufferedImage image = getBufferedImage();
+ return new BufferedImageMonkeyImage(image.getSubimage(x, y, w, h));
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.collect.Lists;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Provides a nicer interface to interacting with the low-level network access protocol for talking
+ * to the monkey.
+ *
+ * This class is thread-safe and can handle being called from multiple threads.
+ */
+public class MonkeyManager {
+ private static Logger LOG = Logger.getLogger(MonkeyManager.class.getName());
+
+ private Socket monkeySocket;
+ private BufferedWriter monkeyWriter;
+ private BufferedReader monkeyReader;
+
+ /**
+ * Create a new MonkeyMananger to talk to the specified device.
+ *
+ * @param monkeySocket the already connected socket on which to send protocol messages.
+ */
+ public MonkeyManager(Socket monkeySocket) {
+ try {
+ this.monkeySocket = monkeySocket;
+ monkeyWriter = new BufferedWriter(new OutputStreamWriter(monkeySocket.getOutputStream()));
+ monkeyReader = new BufferedReader(new InputStreamReader(monkeySocket.getInputStream()));
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Send a touch down event at the specified location.
+ *
+ * @param x the x coordinate of where to click
+ * @param y the y coordinate of where to click
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean touchDown(int x, int y) throws IOException {
+ return sendMonkeyEvent("touch down " + x + " " + y);
+ }
+
+ /**
+ * Send a touch down event at the specified location.
+ *
+ * @param x the x coordinate of where to click
+ * @param y the y coordinate of where to click
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean touchUp(int x, int y) throws IOException {
+ return sendMonkeyEvent("touch up " + x + " " + y);
+ }
+
+ /**
+ * Send a touch move event at the specified location.
+ *
+ * @param x the x coordinate of where to click
+ * @param y the y coordinate of where to click
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean touchMove(int x, int y) throws IOException {
+ return sendMonkeyEvent("touch move " + x + " " + y);
+ }
+
+ /**
+ * Send a touch (down and then up) event at the specified location.
+ *
+ * @param x the x coordinate of where to click
+ * @param y the y coordinate of where to click
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean touch(int x, int y) throws IOException {
+ return sendMonkeyEvent("tap " + x + " " + y);
+ }
+
+ /**
+ * Press a physical button on the device.
+ *
+ * @param name the name of the button (As specified in the protocol)
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean press(String name) throws IOException {
+ return sendMonkeyEvent("press " + name);
+ }
+
+ /**
+ * Send a Key Down event for the specified button.
+ *
+ * @param name the name of the button (As specified in the protocol)
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean keyDown(String name) throws IOException {
+ return sendMonkeyEvent("key down " + name);
+ }
+
+ /**
+ * Send a Key Up event for the specified button.
+ *
+ * @param name the name of the button (As specified in the protocol)
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean keyUp(String name) throws IOException {
+ return sendMonkeyEvent("key up " + name);
+ }
+
+ /**
+ * Press a physical button on the device.
+ *
+ * @param button the button to press
+ * @return success or not
+ * @throws IOException on error communicating with the device
+ */
+ public boolean press(PhysicalButton button) throws IOException {
+ return press(button.getKeyName());
+ }
+
+ /**
+ * This function allows the communication bridge between the host and the device
+ * to be invisible to the script for internal needs.
+ * It splits a command into monkey events and waits for responses for each over an adb tcp socket.
+ * Returns on an error, else continues and sets up last response.
+ *
+ * @param command the monkey command to send to the device
+ * @return the (unparsed) response returned from the monkey.
+ */
+ private String sendMonkeyEventAndGetResponse(String command) throws IOException {
+ command = command.trim();
+ LOG.info("Monkey Command: " + command + ".");
+
+ // send a single command and get the response
+ monkeyWriter.write(command + "\n");
+ monkeyWriter.flush();
+ return monkeyReader.readLine();
+ }
+
+ /**
+ * Parse a monkey response string to see if the command succeeded or not.
+ *
+ * @param monkeyResponse the response
+ * @return true if response code indicated success.
+ */
+ private boolean parseResponseForSuccess(String monkeyResponse) {
+ if (monkeyResponse == null) {
+ return false;
+ }
+ // return on ok
+ if(monkeyResponse.startsWith("OK")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse a monkey response string to get the extra data returned.
+ *
+ * @param monkeyResponse the response
+ * @return any extra data that was returned, or empty string if there was nothing.
+ */
+ private String parseResponseForExtra(String monkeyResponse) {
+ int offset = monkeyResponse.indexOf(':');
+ if (offset < 0) {
+ return "";
+ }
+ return monkeyResponse.substring(offset + 1);
+ }
+
+ /**
+ * This function allows the communication bridge between the host and the device
+ * to be invisible to the script for internal needs.
+ * It splits a command into monkey events and waits for responses for each over an
+ * adb tcp socket.
+ *
+ * @param command the monkey command to send to the device
+ * @return true on success.
+ */
+ private boolean sendMonkeyEvent(String command) throws IOException {
+ synchronized (this) {
+ String monkeyResponse = sendMonkeyEventAndGetResponse(command);
+ return parseResponseForSuccess(monkeyResponse);
+ }
+ }
+
+ /**
+ * Close all open resources related to this device.
+ */
+ public void close() {
+ try {
+ monkeySocket.close();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to close monkeySocket", e);
+ }
+ try {
+ monkeyReader.close();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to close monkeyReader", e);
+ }
+ try {
+ monkeyWriter.close();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to close monkeyWriter", e);
+ }
+ }
+
+ /**
+ * Function to get a static variable from the device.
+ *
+ * @param name name of static variable to get
+ * @return the value of the variable, or null if there was an error
+ */
+ public String getVariable(String name) throws IOException {
+ synchronized (this) {
+ String response = sendMonkeyEventAndGetResponse("getvar " + name);
+ if (!parseResponseForSuccess(response)) {
+ return null;
+ }
+ return parseResponseForExtra(response);
+ }
+ }
+
+ /**
+ * Function to get the list of static variables from the device.
+ */
+ public Collection<String> listVariable() throws IOException {
+ synchronized (this) {
+ String response = sendMonkeyEventAndGetResponse("listvar");
+ if (!parseResponseForSuccess(response)) {
+ Collections.emptyList();
+ }
+ String extras = parseResponseForExtra(response);
+ return Lists.newArrayList(extras.split(" "));
+ }
+ }
+
+ /**
+ * Tells the monkey that we are done for this session.
+ * @throws IOException
+ */
+ public void done() throws IOException {
+ // this command just drops the connection, so handle it here
+ synchronized (this) {
+ sendMonkeyEventAndGetResponse("done");
+ }
+ }
+
+ /**
+ * Tells the monkey that we are done forever.
+ * @throws IOException
+ */
+ public void quit() throws IOException {
+ // this command drops the connection, so handle it here
+ synchronized (this) {
+ sendMonkeyEventAndGetResponse("quit");
+ }
+ }
+
+ /**
+ * Send a tap event at the specified location.
+ *
+ * @param x the x coordinate of where to click
+ * @param y the y coordinate of where to click
+ * @return success or not
+ * @throws IOException
+ * @throws IOException on error communicating with the device
+ */
+ public boolean tap(int x, int y) throws IOException {
+ return sendMonkeyEvent("tap " + x + " " + y);
+ }
+
+ /**
+ * Type the following string to the monkey.
+ *
+ * @param text the string to type
+ * @return success
+ * @throws IOException
+ */
+ public boolean type(String text) throws IOException {
+ // The network protocol can't handle embedded line breaks, so we have to handle it
+ // here instead
+ StringTokenizer tok = new StringTokenizer(text, "\n", true);
+ while (tok.hasMoreTokens()) {
+ String line = tok.nextToken();
+ if ("\n".equals(line)) {
+ boolean success = press(PhysicalButton.ENTER);
+ if (!success) {
+ return false;
+ }
+ } else {
+ boolean success = sendMonkeyEvent("type " + line);
+ if (!success) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Type the character to the monkey.
+ *
+ * @param keyChar the character to type.
+ * @return success
+ * @throws IOException
+ */
+ public boolean type(char keyChar) throws IOException {
+ return type(Character.toString(keyChar));
+ }
+
+ /**
+ * Wake the device up from sleep.
+ * @throws IOException
+ */
+ public void wake() throws IOException {
+ sendMonkeyEvent("wake");
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Functions;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.python.core.ArgParser;
+import org.python.core.PyException;
+import org.python.core.PyObject;
+
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.JOptionPane;
+
+/**
+ * This is the main interface class into the jython bindings.
+ */
+public class MonkeyRunner {
+ private static final Logger LOG = Logger.getLogger(MonkeyRunner.class.getCanonicalName());
+ private static MonkeyRunnerBackend backend;
+
+ /**
+ * Set the backend MonkeyRunner is using.
+ *
+ * @param backend the backend to use.
+ */
+ /* package */ static void setBackend(MonkeyRunnerBackend backend) {
+ MonkeyRunner.backend = backend;
+ }
+
+ @MonkeyRunnerExported(doc = "Wait for the specified device to connect.",
+ args = {"timeout", "deviceId"},
+ argDocs = {"The timeout in seconds to wait for the device to connect. (default " +
+ "is to wait forever)",
+ "A regular expression that specifies the device of for valid devices" +
+ " to wait for."},
+ returns = "A MonkeyDevice representing the connected device.")
+ public static MonkeyDevice waitForConnection(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ long timeoutMs;
+ try {
+ double timeoutInSecs = JythonUtils.getFloat(ap, 0);
+ timeoutMs = (long) (timeoutInSecs * 1000.0);
+ } catch (PyException e) {
+ timeoutMs = Long.MAX_VALUE;
+ }
+
+ return backend.waitForConnection(timeoutMs,
+ ap.getString(1, ".*"));
+ }
+
+ @MonkeyRunnerExported(doc = "Pause script processing for the specified number of seconds",
+ args = {"seconds"},
+ argDocs = {"The number of seconds to pause processing"})
+ public static void sleep(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ double seconds = JythonUtils.getFloat(ap, 0);
+
+ long ms = (long) (seconds * 1000.0);
+
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ LOG.log(Level.SEVERE, "Error sleeping", e);
+ }
+ }
+
+ @MonkeyRunnerExported(doc = "Simple help command to dump the MonkeyRunner supported " +
+ "commands",
+ args = { "format" },
+ argDocs = {"The format to return the help text in. (default is text)"},
+ returns = "The help text")
+ public static String help(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String format = ap.getString(0, "text");
+
+ return MonkeyRunnerHelp.helpString(format);
+ }
+
+ @MonkeyRunnerExported(doc = "Put up an alert dialog to inform the user of something that " +
+ "happened. This is modal dialog and will stop processing of " +
+ "the script until the user acknowledges the alert message",
+ args = { "message", "title", "okTitle" },
+ argDocs = {
+ "The contents of the message of the dialog box",
+ "The title to display for the dialog box. (default value is \"Alert\")",
+ "The title to use for the acknowledgement button (default value is \"OK\")"
+ })
+ public static void alert(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String message = ap.getString(0);
+ String title = ap.getString(1, "Alert");
+ String buttonTitle = ap.getString(2, "OK");
+
+ alert(message, title, buttonTitle);
+ }
+
+ @MonkeyRunnerExported(doc = "Put up an input dialog that allows the user to input a string." +
+ " This is a modal dialog that will stop processing of the script until the user " +
+ "inputs the requested information.",
+ args = {"message", "initialValue", "title", "okTitle", "cancelTitle"},
+ argDocs = {
+ "The message to display for the input.",
+ "The initial value to supply the user (default is empty string)",
+ "The title of the dialog box to display. (default is \"Input\")"
+ },
+ returns = "The test entered by the user, or None if the user canceled the input;"
+ )
+ public static String input(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String message = ap.getString(0);
+ String initialValue = ap.getString(1, "");
+ String title = ap.getString(2, "Input");
+
+ return input(message, initialValue, title);
+ }
+
+ @MonkeyRunnerExported(doc = "Put up a choice dialog that allows the user to select a single " +
+ "item from a list of items that were presented.",
+ args = {"message", "choices", "title"},
+ argDocs = {
+ "The message to display for the input.",
+ "The list of choices to display.",
+ "The title of the dialog box to display. (default is \"Input\")" },
+ returns = "The numeric offset of the choice selected.")
+ public static int choice(PyObject[] args, String kws[]) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String message = ap.getString(0);
+ Collection<String> choices = Collections2.transform(JythonUtils.getList(ap, 1),
+ Functions.toStringFunction());
+ String title = ap.getString(2, "Input");
+
+ return choice(message, title, choices);
+ }
+
+ /**
+ * Display an alert dialog.
+ *
+ * @param message the message to show.
+ * @param title the title of the dialog box.
+ * @param okTitle the title of the button.
+ */
+ private static void alert(String message, String title, String okTitle) {
+ Object[] options = { okTitle };
+ JOptionPane.showOptionDialog(null, message, title, JOptionPane.DEFAULT_OPTION,
+ JOptionPane.INFORMATION_MESSAGE, null, options, options[0]);
+ }
+
+ /**
+ * Display a dialog allow the user to pick a choice from a list of choices.
+ *
+ * @param message the message to show.
+ * @param title the title of the dialog box.
+ * @param choices the list of the choices to display.
+ * @return the index of the selected choice, or -1 if nothing was chosen.
+ */
+ private static int choice(String message, String title, Collection<String> choices) {
+ Object[] possibleValues = choices.toArray();
+ Object selectedValue = JOptionPane.showInputDialog(null, message, title,
+ JOptionPane.QUESTION_MESSAGE, null, possibleValues, possibleValues[0]);
+
+ for (int x = 0; x < possibleValues.length; x++) {
+ if (possibleValues[x].equals(selectedValue)) {
+ return x;
+ }
+ }
+ // Error
+ return -1;
+ }
+
+ /**
+ * Display a dialog that allows the user to input a text string.
+ *
+ * @param message the message to show.
+ * @param initialValue the initial value to display in the dialog
+ * @param title the title of the dialog box.
+ * @return the entered string, or null if cancelled
+ */
+ private static String input(String message, String initialValue, String title) {
+ return (String) JOptionPane.showInputDialog(null, message, title,
+ JOptionPane.QUESTION_MESSAGE, null, null, initialValue);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+/**
+ * Interface between MonkeyRunner common code and the MonkeyRunner backend. The backend is
+ * responsible for communicating between the host and the device.
+ */
+public interface MonkeyRunnerBackend {
+ /**
+ * Wait for a device to connect to the backend.
+ *
+ * @param timeoutMs how long (in ms) to wait
+ * @param deviceIdRegex the regular expression to specify which device to wait for.
+ * @return the connected device (or null if timeout);
+ */
+ MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex);
+
+ /**
+ * Shutdown the backend and cleanup any resources it was using.
+ */
+ void shutdown();
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.io.Resources;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.clearsilver.CS;
+import org.clearsilver.CSFileLoader;
+import org.clearsilver.HDF;
+
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility class for generating inline help documentation
+ */
+public final class MonkeyRunnerHelp {
+ private MonkeyRunnerHelp() { }
+
+ private static final String HELP = "help";
+ private static final String NAME = "name";
+ private static final String DOC = "doc";
+ private static final String ARGUMENT = "argument";
+ private static final String RETURNS = "returns";
+ private static final String TYPE = "type";
+
+ // Enum used to describe documented types.
+ private enum Type {
+ ENUM, FIELD, METHOD
+ }
+
+ private static void getAllExportedClasses(Set<Field> fields,
+ Set<Method> methods,
+ Set<Constructor<?>> constructors,
+ Set<Class<?>> enums) {
+ final Set<Class<?>> classesVisited = Sets.newHashSet();
+ Set<Class<?>> classesToVisit = Sets.newHashSet();
+ classesToVisit.add(MonkeyRunner.class);
+
+ Predicate<Class<?>> haventSeen = new Predicate<Class<?>>() {
+ public boolean apply(Class<?> clz) {
+ return !classesVisited.contains(clz);
+ }
+ };
+
+ while (!classesToVisit.isEmpty()) {
+ classesVisited.addAll(classesToVisit);
+
+ List<Class<?>> newClasses = Lists.newArrayList();
+ for (Class<?> clz : classesToVisit) {
+ // See if the class itself is annotated and is an enum
+ if (clz.isEnum() && clz.isAnnotationPresent(MonkeyRunnerExported.class)) {
+ enums.add(clz);
+ }
+
+ // Constructors
+ for (Constructor<?> c : clz.getConstructors()) {
+ newClasses.addAll(Collections2.filter(Arrays.asList(c.getParameterTypes()),
+ haventSeen));
+ if (c.isAnnotationPresent(MonkeyRunnerExported.class)) {
+ constructors.add(c);
+ }
+ }
+
+ // Fields
+ for (Field f : clz.getFields()) {
+ if (haventSeen.apply(f.getClass())) {
+ newClasses.add(f.getClass());
+ }
+ if (f.isAnnotationPresent(MonkeyRunnerExported.class)) {
+ fields.add(f);
+ }
+ }
+
+ // Methods
+ for (Method m : clz.getMethods()) {
+ newClasses.addAll(Collections2.filter(Arrays.asList(m.getParameterTypes()),
+ haventSeen));
+ if (haventSeen.apply(m.getReturnType())) {
+ newClasses.add(m.getReturnType());
+ }
+
+ if (m.isAnnotationPresent(MonkeyRunnerExported.class)) {
+ methods.add(m);
+ }
+ }
+
+ // Containing classes
+ for (Class<?> toAdd : clz.getClasses()) {
+ if (haventSeen.apply(toAdd)) {
+ newClasses.add(toAdd);
+ }
+ }
+ }
+
+ classesToVisit.clear();
+ classesToVisit.addAll(newClasses);
+ }
+ }
+
+ private static Comparator<Member> MEMBER_SORTER = new Comparator<Member>() {
+ public int compare(Member o1, Member o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ };
+
+ private static Comparator<Class<?>> CLASS_SORTER = new Comparator<Class<?>>() {
+ public int compare(Class<?> o1, Class<?> o2) {
+ return o1.getName().compareTo(o2.getName());
+ }
+ };
+
+ public static String helpString(String format) {
+ // Quick check for support formats
+ if ("html".equals(format) || "text".equals(format)) {
+ HDF hdf = buildHelpHdf();
+ CS clearsilver = new CS(hdf);
+ // Set a custom file loader to load requested files from resources relative to this class.
+ clearsilver.setFileLoader(new CSFileLoader() {
+ public String load(HDF hdf, String filename) throws IOException {
+ return Resources.toString(Resources.getResource(MonkeyRunnerHelp.class, filename),
+ Charset.defaultCharset());
+ }
+ });
+
+ // Load up the CS template file
+ clearsilver.parseFile(format.toLowerCase() + ".cs");
+ // And render the output
+ return clearsilver.render();
+ } else if ("hdf".equals(format)) {
+ HDF hdf = buildHelpHdf();
+ return hdf.writeString();
+ }
+ return "";
+ }
+
+ private static HDF buildHelpHdf() {
+ HDF hdf = new HDF();
+
+ int outputItemCount = 0;
+
+ Set<Field> fields = Sets.newTreeSet(MEMBER_SORTER);
+ Set<Method> methods = Sets.newTreeSet(MEMBER_SORTER);
+ Set<Constructor<?>> constructors = Sets.newTreeSet(MEMBER_SORTER);
+ Set<Class<?>> classes = Sets.newTreeSet(CLASS_SORTER);
+ getAllExportedClasses(fields, methods, constructors, classes);
+
+ for (Class<?> clz : classes) {
+ String prefix = HELP + "." + outputItemCount + ".";
+
+ hdf.setValue(prefix + NAME, clz.getCanonicalName());
+ MonkeyRunnerExported annotation = clz.getAnnotation(MonkeyRunnerExported.class);
+ hdf.setValue(prefix + DOC, annotation.doc());
+ hdf.setValue(prefix + TYPE, Type.ENUM.name());
+
+ // Now go through the enumeration constants
+ Object[] constants = clz.getEnumConstants();
+ String[] argDocs = annotation.argDocs();
+ if (constants.length > 0) {
+ for (int x = 0; x < constants.length; x++) {
+ String argPrefix = prefix + ARGUMENT + "." + x + ".";
+ hdf.setValue(argPrefix + NAME, constants[x].toString());
+ if (argDocs.length > x) {
+ hdf.setValue(argPrefix + DOC, argDocs[x]);
+ }
+ }
+ }
+ outputItemCount++;
+ }
+
+ for (Method m : methods) {
+ String prefix = HELP + "." + outputItemCount + ".";
+
+ MonkeyRunnerExported annotation = m.getAnnotation(MonkeyRunnerExported.class);
+ String className = m.getDeclaringClass().getCanonicalName();
+ String methodName = className + "." + m.getName();
+ hdf.setValue(prefix + NAME, methodName);
+ hdf.setValue(prefix + DOC, annotation.doc());
+ if (annotation.args().length > 0) {
+ String[] argDocs = annotation.argDocs();
+ String[] aargs = annotation.args();
+ for (int x = 0; x < aargs.length; x++) {
+ String argPrefix = prefix + ARGUMENT + "." + x + ".";
+
+ hdf.setValue(argPrefix + NAME, aargs[x]);
+ if (argDocs.length > x) {
+ hdf.setValue(argPrefix + DOC, argDocs[x]);
+ }
+ }
+ }
+ if (!"".equals(annotation.returns())) {
+ hdf.setValue(prefix + RETURNS, annotation.returns());
+ }
+ outputItemCount++;
+ }
+
+ return hdf;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class MonkeyRunnerOptions {
+ private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName());
+ private static String DEFAULT_MONKEY_SERVER_ADDRESS = "127.0.0.1";
+ private static int DEFAULT_MONKEY_PORT = 12345;
+
+ private final int port;
+ private final String hostname;
+ private final File scriptFile;
+ private final String backend;
+ private final Collection<File> plugins;
+ private final Collection<String> arguments;
+
+ private MonkeyRunnerOptions(String hostname, int port, File scriptFile, String backend,
+ Collection<File> plugins, Collection<String> arguments) {
+ this.hostname = hostname;
+ this.port = port;
+ this.scriptFile = scriptFile;
+ this.backend = backend;
+ this.plugins = plugins;
+ this.arguments = arguments;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public File getScriptFile() {
+ return scriptFile;
+ }
+
+ public String getBackendName() {
+ return backend;
+ }
+
+ public Collection<File> getPlugins() {
+ return plugins;
+ }
+
+ public Collection<String> getArguments() {
+ return arguments;
+ }
+
+ private static void printUsage(String message) {
+ System.out.println(message);
+ System.out.println("Usage: monkeyrunner [options] SCRIPT_FILE");
+ System.out.println("");
+ System.out.println(" -s MonkeyServer IP Address.");
+ System.out.println(" -p MonkeyServer TCP Port.");
+ System.out.println(" -v MonkeyServer Logging level (ALL, FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, OFF)");
+ System.out.println("");
+ System.out.println("");
+ }
+
+ /**
+ * Process the command-line options
+ *
+ * @return the parsed options, or null if there was an error.
+ */
+ public static MonkeyRunnerOptions processOptions(String[] args) {
+ // parse command line parameters.
+ int index = 0;
+
+ String hostname = DEFAULT_MONKEY_SERVER_ADDRESS;
+ File scriptFile = null;
+ int port = DEFAULT_MONKEY_PORT;
+ String backend = "adb";
+
+ ImmutableList.Builder<File> pluginListBuilder = ImmutableList.builder();
+ ImmutableList.Builder<String> argumentBuilder = ImmutableList.builder();
+ while (index < args.length) {
+ String argument = args[index++];
+
+ if ("-s".equals(argument)) {
+ if (index == args.length) {
+ printUsage("Missing Server after -s");
+ return null;
+ }
+ hostname = args[index++];
+
+ } else if ("-p".equals(argument)) {
+ // quick check on the next argument.
+ if (index == args.length) {
+ printUsage("Missing Server port after -p");
+ return null;
+ }
+ port = Integer.parseInt(args[index++]);
+
+ } else if ("-v".equals(argument)) {
+ // quick check on the next argument.
+ if (index == args.length) {
+ printUsage("Missing Log Level after -v");
+ return null;
+ }
+
+ Level level = Level.parse(args[index++]);
+ LOG.setLevel(level);
+ level = LOG.getLevel();
+ System.out.println("Log level set to: " + level + "(" + level.intValue() + ").");
+ System.out.println("Warning: Log levels below INFO(800) not working currently... parent issues");
+ } else if ("-be".equals(argument)) {
+ // quick check on the next argument.
+ if (index == args.length) {
+ printUsage("Missing backend name after -be");
+ return null;
+ }
+ backend = args[index++];
+ } else if ("-plugin".equals(argument)) {
+ // quick check on the next argument.
+ if (index == args.length) {
+ printUsage("Missing plugin path after -plugin");
+ return null;
+ }
+ File plugin = new File(args[index++]);
+ if (!plugin.exists()) {
+ printUsage("Plugin file doesn't exist");
+ return null;
+ }
+
+ if (!plugin.canRead()) {
+ printUsage("Can't read plugin file");
+ return null;
+ }
+
+ pluginListBuilder.add(plugin);
+ } else if (argument.startsWith("-") &&
+ // Once we have the scriptfile, the rest of the arguments go to jython.
+ scriptFile == null) {
+ // we have an unrecognized argument.
+ printUsage("Unrecognized argument: " + argument + ".");
+ return null;
+ } else {
+ if (scriptFile == null) {
+ // get the filepath of the script to run. This will be the last undashed argument.
+ scriptFile = new File(argument);
+ if (!scriptFile.exists()) {
+ printUsage("Can't open specified script file");
+ return null;
+ }
+ if (!scriptFile.canRead()) {
+ printUsage("Can't open specified script file");
+ return null;
+ }
+ } else {
+ argumentBuilder.add(argument);
+ }
+ }
+ };
+
+ return new MonkeyRunnerOptions(hostname, port, scriptFile, backend,
+ pluginListBuilder.build(), argumentBuilder.build());
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableMap;
+
+import com.android.monkeyrunner.adb.AdbBackend;
+import com.android.monkeyrunner.stub.StubBackend;
+
+import org.python.util.PythonInterpreter;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+
+/**
+ * MonkeyRunner is a host side application to control a monkey instance on a
+ * device. MonkeyRunner provides some useful helper functions to control the
+ * device as well as various other methods to help script tests. This class bootstraps
+ * MonkeyRunner.
+ */
+public class MonkeyRunnerStarter {
+ private static final Logger LOG = Logger.getLogger(MonkeyRunnerStarter.class.getName());
+ private static final String MONKEY_RUNNER_MAIN_MANIFEST_NAME = "MonkeyRunnerStartupRunner";
+
+ private final MonkeyRunnerBackend backend;
+ private final MonkeyRunnerOptions options;
+
+ public MonkeyRunnerStarter(MonkeyRunnerOptions options) {
+ this.options = options;
+ this.backend = MonkeyRunnerStarter.createBackendByName(options.getBackendName());
+ if (this.backend == null) {
+ throw new RuntimeException("Unknown backend");
+ }
+ }
+
+
+ /**
+ * Creates a specific backend by name.
+ *
+ * @param backendName the name of the backend to create
+ * @return the new backend, or null if none were found.
+ */
+ public static MonkeyRunnerBackend createBackendByName(String backendName) {
+ if ("adb".equals(backendName)) {
+ return new AdbBackend();
+ } else if ("stub".equals(backendName)) {
+ return new StubBackend();
+ } else {
+ return null;
+ }
+ }
+
+ private int run() {
+ MonkeyRunner.setBackend(backend);
+ Map<String, Predicate<PythonInterpreter>> plugins = handlePlugins();
+ if (options.getScriptFile() == null) {
+ ScriptRunner.console();
+ return 0;
+ } else {
+ int error = ScriptRunner.run(options.getScriptFile().getAbsolutePath(),
+ options.getArguments(), plugins);
+ backend.shutdown();
+ MonkeyRunner.setBackend(null);
+ return error;
+ }
+ }
+
+ private Predicate<PythonInterpreter> handlePlugin(File f) {
+ JarFile jarFile;
+ try {
+ jarFile = new JarFile(f);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to open plugin file. Is it a jar file? " +
+ f.getAbsolutePath(), e);
+ return Predicates.alwaysFalse();
+ }
+ Manifest manifest;
+ try {
+ manifest = jarFile.getManifest();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to get manifest file from jar: " +
+ f.getAbsolutePath(), e);
+ return Predicates.alwaysFalse();
+ }
+ Attributes mainAttributes = manifest.getMainAttributes();
+ String pluginClass = mainAttributes.getValue(MONKEY_RUNNER_MAIN_MANIFEST_NAME);
+ if (pluginClass == null) {
+ // No main in this plugin, so it always succeeds.
+ return Predicates.alwaysTrue();
+ }
+ URL url;
+ try {
+ url = f.toURI().toURL();
+ } catch (MalformedURLException e) {
+ LOG.log(Level.SEVERE, "Unable to convert file to url " + f.getAbsolutePath(),
+ e);
+ return Predicates.alwaysFalse();
+ }
+ URLClassLoader classLoader = new URLClassLoader(new URL[] { url },
+ ClassLoader.getSystemClassLoader());
+ Class<?> clz;
+ try {
+ clz = Class.forName(pluginClass, true, classLoader);
+ } catch (ClassNotFoundException e) {
+ LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e);
+ return Predicates.alwaysFalse();
+ }
+ Object loadedObject;
+ try {
+ loadedObject = clz.newInstance();
+ } catch (InstantiationException e) {
+ LOG.log(Level.SEVERE, "Unable to load the specified plugin: " + pluginClass, e);
+ return Predicates.alwaysFalse();
+ } catch (IllegalAccessException e) {
+ LOG.log(Level.SEVERE, "Unable to load the specified plugin " +
+ "(did you make it public?): " + pluginClass, e);
+ return Predicates.alwaysFalse();
+ }
+ // Cast it to the right type
+ if (loadedObject instanceof Runnable) {
+ final Runnable run = (Runnable) loadedObject;
+ return new Predicate<PythonInterpreter>() {
+ public boolean apply(PythonInterpreter i) {
+ run.run();
+ return true;
+ }
+ };
+ } else if (loadedObject instanceof Predicate<?>) {
+ return (Predicate<PythonInterpreter>) loadedObject;
+ } else {
+ LOG.severe("Unable to coerce object into correct type: " + pluginClass);
+ return Predicates.alwaysFalse();
+ }
+ }
+
+ private Map<String, Predicate<PythonInterpreter>> handlePlugins() {
+ ImmutableMap.Builder<String, Predicate<PythonInterpreter>> builder = ImmutableMap.builder();
+ for (File f : options.getPlugins()) {
+ builder.put(f.getAbsolutePath(), handlePlugin(f));
+ }
+ return builder.build();
+ }
+
+
+
+ private static final void replaceAllLogFormatters(Formatter form) {
+ LogManager mgr = LogManager.getLogManager();
+ Enumeration<String> loggerNames = mgr.getLoggerNames();
+ while (loggerNames.hasMoreElements()) {
+ String loggerName = loggerNames.nextElement();
+ Logger logger = mgr.getLogger(loggerName);
+ for (Handler handler : logger.getHandlers()) {
+ handler.setFormatter(form);
+ handler.setLevel(Level.INFO);
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ MonkeyRunnerOptions options = MonkeyRunnerOptions.processOptions(args);
+
+ // logging property files are difficult
+ replaceAllLogFormatters(MonkeyFormatter.DEFAULT_INSTANCE);
+
+ if (options == null) {
+ return;
+ }
+
+ MonkeyRunnerStarter runner = new MonkeyRunnerStarter(options);
+ int error = runner.run();
+
+ // This will kill any background threads as well.
+ System.exit(error);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+public enum PhysicalButton {
+ HOME("home"),
+ SEARCH("search"),
+ MENU("menu"),
+ BACK("back"),
+ DPAD_UP("DPAD_UP"),
+ DPAD_DOWN("DPAD_DOWN"),
+ DPAD_LEFT("DPAD_LEFT"),
+ DPAD_RIGHT("DPAD_RIGHT"),
+ DPAD_CENTER("DPAD_CENTER"),
+ ENTER("enter");
+
+ private String keyName;
+
+ private PhysicalButton(String keyName) {
+ this.keyName = keyName;
+ }
+
+ public String getKeyName() {
+ return keyName;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.ImmutableMap.Builder;
+
+import org.python.core.Py;
+import org.python.core.PyException;
+import org.python.core.PyObject;
+import org.python.util.InteractiveConsole;
+import org.python.util.JLineConsole;
+import org.python.util.PythonInterpreter;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Runs Jython based scripts.
+ */
+public class ScriptRunner {
+ private static final Logger LOG = Logger.getLogger(MonkeyRunnerOptions.class.getName());
+
+ /** The "this" scope object for scripts. */
+ private final Object scope;
+ private final String variable;
+
+ /** Private constructor. */
+ private ScriptRunner(Object scope, String variable) {
+ this.scope = scope;
+ this.variable = variable;
+ }
+
+ /** Creates a new instance for the given scope object. */
+ public static ScriptRunner newInstance(Object scope, String variable) {
+ return new ScriptRunner(scope, variable);
+ }
+
+ /**
+ * Runs the specified Jython script. First runs the initialization script to
+ * preload the appropriate client library version.
+ *
+ * @param scriptfilename the name of the file to run.
+ * @param args the arguments passed in (excluding the filename).
+ * @param plugins a list of plugins to load.
+ * @return the error code from running the script.
+ */
+ public static int run(String scriptfilename, Collection<String> args,
+ Map<String, Predicate<PythonInterpreter>> plugins) {
+ // Add the current directory of the script to the python.path search path.
+ File f = new File(scriptfilename);
+
+ // Adjust the classpath so jython can access the classes in the specified classpath.
+ Collection<String> classpath = Lists.newArrayList(f.getParent());
+ classpath.addAll(plugins.keySet());
+
+ String[] argv = new String[args.size() + 1];
+ argv[0] = f.getAbsolutePath();
+ int x = 1;
+ for (String arg : args) {
+ argv[x++] = arg;
+ }
+
+ initPython(classpath, argv);
+
+ PythonInterpreter python = new PythonInterpreter();
+
+ // Now let the mains run.
+ for (Map.Entry<String, Predicate<PythonInterpreter>> entry : plugins.entrySet()) {
+ boolean success;
+ try {
+ success = entry.getValue().apply(python);
+ } catch (Exception e) {
+ LOG.log(Level.SEVERE, "Plugin Main through an exception.", e);
+ continue;
+ }
+ if (!success) {
+ LOG.severe("Plugin Main returned error for: " + entry.getKey());
+ }
+ }
+
+ // Bind __name__ to __main__ so mains will run
+ python.set("__name__", "__main__");
+
+ try {
+ python.execfile(scriptfilename);
+ } catch (PyException e) {
+ if (Py.SystemExit.equals(e.type)) {
+ // Then recover the error code so we can pass it on
+ return (Integer) e.value.__tojava__(Integer.class);
+ }
+ // Then some other kind of exception was thrown. Log it and return error;
+ LOG.log(Level.SEVERE, "Script terminated due to an exception", e);
+ return 1;
+ }
+ return 0;
+ }
+
+ public static void runString(String script) {
+ initPython();
+ PythonInterpreter python = new PythonInterpreter();
+ python.exec(script);
+ }
+
+ public static Map<String, PyObject> runStringAndGet(String script, String... names) {
+ return runStringAndGet(script, Arrays.asList(names));
+ }
+
+ public static Map<String, PyObject> runStringAndGet(String script, Collection<String> names) {
+ initPython();
+ final PythonInterpreter python = new PythonInterpreter();
+ python.exec(script);
+
+ Builder<String, PyObject> builder = ImmutableMap.builder();
+ for (String name : names) {
+ builder.put(name, python.get(name));
+ }
+ return builder.build();
+ }
+
+ private static void initPython() {
+ List<String> arg = Collections.emptyList();
+ initPython(arg, new String[] {""});
+ }
+
+ private static void initPython(Collection<String> pythonPath,
+ String[] argv) {
+ Properties props = new Properties();
+
+ // Build up the python.path
+ StringBuilder sb = new StringBuilder();
+ sb.append(System.getProperty("java.class.path"));
+ for (String p : pythonPath) {
+ sb.append(":").append(p);
+ }
+ props.setProperty("python.path", sb.toString());
+
+ /** Initialize the python interpreter. */
+ // Default is 'message' which displays sys-package-mgr bloat
+ // Choose one of error,warning,message,comment,debug
+ props.setProperty("python.verbose", "error");
+
+ PythonInterpreter.initialize(System.getProperties(), props, argv);
+ }
+
+ /**
+ * Start an interactive python interpreter.
+ */
+ public static void console() {
+ initPython();
+ InteractiveConsole python = new JLineConsole();
+ python.interact();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.google.common.collect.Lists;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.IDevice;
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.MonkeyRunnerBackend;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * Backend implementation that works over ADB to talk to the device.
+ */
+public class AdbBackend implements MonkeyRunnerBackend {
+ private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName());
+ // How long to wait each time we check for the device to be connected.
+ private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200;
+ private final List<AdbMonkeyDevice> devices = Lists.newArrayList();
+
+ private final AndroidDebugBridge bridge;
+
+ public AdbBackend() {
+ AndroidDebugBridge.init(false /* debugger support */);
+
+ bridge = AndroidDebugBridge.createBridge(
+ "adb", true /* forceNewBridge */);
+ }
+
+ /**
+ * Checks the attached devices looking for one whose device id matches the specified regex.
+ *
+ * @param deviceIdRegex the regular expression to match against
+ * @return the Device (if found), or null (if not found).
+ */
+ private IDevice findAttacedDevice(String deviceIdRegex) {
+ Pattern pattern = Pattern.compile(deviceIdRegex);
+ for (IDevice device : bridge.getDevices()) {
+ String serialNumber = device.getSerialNumber();
+ if (pattern.matcher(serialNumber).matches()) {
+ return device;
+ }
+ }
+ return null;
+ }
+
+ public MonkeyDevice waitForConnection() {
+ return waitForConnection(Integer.MAX_VALUE, ".*");
+ }
+
+ public MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex) {
+ do {
+ IDevice device = findAttacedDevice(deviceIdRegex);
+ if (device != null) {
+ AdbMonkeyDevice amd = new AdbMonkeyDevice(device);
+ devices.add(amd);
+ return amd;
+ }
+
+ try {
+ Thread.sleep(CONNECTION_ITERATION_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ LOG.log(Level.SEVERE, "Error sleeping", e);
+ }
+ timeoutMs -= CONNECTION_ITERATION_TIMEOUT_MS;
+ } while (timeoutMs > 0);
+
+ // Timeout. Give up.
+ return null;
+ }
+
+ public void shutdown() {
+ for (AdbMonkeyDevice device : devices) {
+ device.dispose();
+ }
+ AndroidDebugBridge.terminate();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.InstallException;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.MonkeyImage;
+import com.android.monkeyrunner.MonkeyManager;
+import com.android.monkeyrunner.adb.LinearInterpolator.Point;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+public class AdbMonkeyDevice extends MonkeyDevice {
+ private static final Logger LOG = Logger.getLogger(AdbMonkeyDevice.class.getName());
+
+ private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0];
+ private static final long MANAGER_CREATE_TIMEOUT_MS = 5 * 1000; // 5 seconds
+
+ private final ExecutorService executor = Executors.newCachedThreadPool();
+
+ private final IDevice device;
+ private MonkeyManager manager;
+
+ public AdbMonkeyDevice(IDevice device) {
+ this.device = device;
+ this.manager = createManager("127.0.0.1", 12345);
+
+ Preconditions.checkNotNull(this.manager);
+ }
+
+ @Override
+ public MonkeyManager getManager() {
+ return manager;
+ }
+
+ @Override
+ public void dispose() {
+ try {
+ manager.quit();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error getting the manager to quit", e);
+ }
+ manager = null;
+ }
+
+ private void executeAsyncCommand(final String command,
+ final LoggingOutputReceiver logger) {
+ executor.submit(new Runnable() {
+ public void run() {
+ try {
+ device.executeShellCommand(command, logger);
+ } catch (TimeoutException e) {
+ LOG.log(Level.SEVERE, "Error starting command: " + command, e);
+ throw new RuntimeException(e);
+ } catch (AdbCommandRejectedException e) {
+ LOG.log(Level.SEVERE, "Error starting command: " + command, e);
+ throw new RuntimeException(e);
+ } catch (ShellCommandUnresponsiveException e) {
+ LOG.log(Level.SEVERE, "Error starting command: " + command, e);
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error starting command: " + command, e);
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+
+ private MonkeyManager createManager(String address, int port) {
+ try {
+ device.createForward(port, port);
+ } catch (TimeoutException e) {
+ LOG.log(Level.SEVERE, "Timeout creating adb port forwarding", e);
+ return null;
+ } catch (AdbCommandRejectedException e) {
+ LOG.log(Level.SEVERE, "Adb rejected adb port forwarding command: " + e.getMessage(), e);
+ return null;
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to create adb port forwarding: " + e.getMessage(), e);
+ return null;
+ }
+
+ String command = "monkey --port " + port;
+ executeAsyncCommand(command, new LoggingOutputReceiver(LOG, Level.FINE));
+
+ // Sleep for a second to give the command time to execute.
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ LOG.log(Level.SEVERE, "Unable to sleep", e);
+ }
+
+ InetAddress addr;
+ try {
+ addr = InetAddress.getByName(address);
+ } catch (UnknownHostException e) {
+ LOG.log(Level.SEVERE, "Unable to convert address into InetAddress: " + address, e);
+ return null;
+ }
+
+ // We have a tough problem to solve here. "monkey" on the device gives us no indication
+ // when it has started up and is ready to serve traffic. If you try too soon, commands
+ // will fail. To remedy this, we will keep trying until a single command (in this case,
+ // wake) succeeds.
+ boolean success = false;
+ MonkeyManager mm = null;
+ long start = System.currentTimeMillis();
+
+ while (!success) {
+ long now = System.currentTimeMillis();
+ long diff = now - start;
+ if (diff > MANAGER_CREATE_TIMEOUT_MS) {
+ LOG.severe("Timeout while trying to create monkey mananger");
+ return null;
+ }
+
+ Socket monkeySocket;
+ try {
+ monkeySocket = new Socket(addr, port);
+ } catch (IOException e) {
+ LOG.log(Level.FINE, "Unable to connect socket", e);
+ success = false;
+ continue;
+ }
+
+ mm = new MonkeyManager(monkeySocket);
+
+ try {
+ mm.wake();
+ } catch (IOException e) {
+ LOG.log(Level.FINE, "Unable to wake up device", e);
+ success = false;
+ continue;
+ }
+ success = true;
+ }
+
+ return mm;
+ }
+
+ @Override
+ public MonkeyImage takeSnapshot() {
+ try {
+ return new AdbMonkeyImage(device.getScreenshot());
+ } catch (TimeoutException e) {
+ LOG.log(Level.SEVERE, "Unable to take snapshot", e);
+ return null;
+ } catch (AdbCommandRejectedException e) {
+ LOG.log(Level.SEVERE, "Unable to take snapshot", e);
+ return null;
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to take snapshot", e);
+ return null;
+ }
+ }
+
+ @Override
+ protected String getSystemProperty(String key) {
+ return device.getProperty(key);
+ }
+
+ @Override
+ protected String getProperty(String key) {
+ try {
+ return manager.getVariable(key);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to get variable: " + key, e);
+ return null;
+ }
+ }
+
+ @Override
+ protected void wake() {
+ try {
+ manager.wake();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to wake device (too sleepy?)", e);
+ }
+ }
+
+ private String shell(String... args) {
+ StringBuilder cmd = new StringBuilder();
+ for (String arg : args) {
+ cmd.append(arg).append(" ");
+ }
+ return shell(cmd.toString());
+ }
+
+ @Override
+ protected String shell(String cmd) {
+ CommandOutputCapture capture = new CommandOutputCapture();
+ try {
+ device.executeShellCommand(cmd, capture);
+ } catch (TimeoutException e) {
+ LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
+ return null;
+ } catch (ShellCommandUnresponsiveException e) {
+ LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
+ return null;
+ } catch (AdbCommandRejectedException e) {
+ LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
+ return null;
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error executing command: " + cmd, e);
+ return null;
+ }
+ return capture.toString();
+ }
+
+ @Override
+ protected boolean installPackage(String path) {
+ try {
+ String result = device.installPackage(path, true);
+ if (result != null) {
+ LOG.log(Level.SEVERE, "Got error installing package: "+ result);
+ return false;
+ }
+ return true;
+ } catch (InstallException e) {
+ LOG.log(Level.SEVERE, "Error installing package: " + path, e);
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean removePackage(String packageName) {
+ try {
+ String result = device.uninstallPackage(packageName);
+ if (result != null) {
+ LOG.log(Level.SEVERE, "Got error uninstalling package "+ packageName + ": " +
+ result);
+ return false;
+ }
+ return true;
+ } catch (InstallException e) {
+ LOG.log(Level.SEVERE, "Error installing package: " + packageName, e);
+ return false;
+ }
+ }
+
+ @Override
+ protected void press(String keyName, TouchPressType type) {
+ try {
+ switch (type) {
+ case DOWN_AND_UP:
+ manager.press(keyName);
+ break;
+ case DOWN:
+ manager.keyDown(keyName);
+ break;
+ case UP:
+ manager.keyUp(keyName);
+ break;
+ }
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending press event: " + keyName + " " + type, e);
+ }
+ }
+
+ @Override
+ protected void type(String string) {
+ try {
+ manager.type(string);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error Typing: " + string, e);
+ }
+ }
+
+ @Override
+ protected void touch(int x, int y, TouchPressType type) {
+ try {
+ switch (type) {
+ case DOWN:
+ manager.touchDown(x, y);
+ break;
+ case UP:
+ manager.touchUp(x, y);
+ break;
+ case DOWN_AND_UP:
+ manager.tap(x, y);
+ break;
+ }
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending touch event: " + x + " " + y + " " + type, e);
+ }
+ }
+
+ @Override
+ protected void reboot(String into) {
+ try {
+ device.reboot(into);
+ } catch (TimeoutException e) {
+ LOG.log(Level.SEVERE, "Unable to reboot device", e);
+ } catch (AdbCommandRejectedException e) {
+ LOG.log(Level.SEVERE, "Unable to reboot device", e);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Unable to reboot device", e);
+ }
+ }
+
+ @Override
+ protected void startActivity(String uri, String action, String data, String mimetype,
+ Collection<String> categories, Map<String, Object> extras, String component,
+ int flags) {
+ List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories,
+ extras, component, flags);
+ shell(Lists.asList("am", "start",
+ intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY));
+ }
+
+ @Override
+ protected void broadcastIntent(String uri, String action, String data, String mimetype,
+ Collection<String> categories, Map<String, Object> extras, String component,
+ int flags) {
+ List<String> intentArgs = buildIntentArgString(uri, action, data, mimetype, categories,
+ extras, component, flags);
+ shell(Lists.asList("am", "broadcast",
+ intentArgs.toArray(ZERO_LENGTH_STRING_ARRAY)).toArray(ZERO_LENGTH_STRING_ARRAY));
+ }
+
+ private static boolean isNullOrEmpty(@Nullable String string) {
+ return string == null || string.length() == 0;
+ }
+
+ private List<String> buildIntentArgString(String uri, String action, String data, String mimetype,
+ Collection<String> categories, Map<String, Object> extras, String component,
+ int flags) {
+ List<String> parts = Lists.newArrayList();
+
+ // from adb docs:
+ //<INTENT> specifications include these flags:
+ // [-a <ACTION>] [-d <DATA_URI>] [-t <MIME_TYPE>]
+ // [-c <CATEGORY> [-c <CATEGORY>] ...]
+ // [-e|--es <EXTRA_KEY> <EXTRA_STRING_VALUE> ...]
+ // [--esn <EXTRA_KEY> ...]
+ // [--ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE> ...]
+ // [-e|--ei <EXTRA_KEY> <EXTRA_INT_VALUE> ...]
+ // [-n <COMPONENT>] [-f <FLAGS>]
+ // [<URI>]
+
+ if (!isNullOrEmpty(action)) {
+ parts.add("-a");
+ parts.add(action);
+ }
+
+ if (!isNullOrEmpty(data)) {
+ parts.add("-d");
+ parts.add(data);
+ }
+
+ if (!isNullOrEmpty(mimetype)) {
+ parts.add("-t");
+ parts.add(mimetype);
+ }
+
+ // Handle categories
+ for (String category : categories) {
+ parts.add("-c");
+ parts.add(category);
+ }
+
+ // Handle extras
+ for (Entry<String, Object> entry : extras.entrySet()) {
+ // Extras are either boolean, string, or int. See which we have
+ Object value = entry.getValue();
+ String valueString;
+ String arg;
+ if (value instanceof Integer) {
+ valueString = Integer.toString((Integer) value);
+ arg = "--ei";
+ } else if (value instanceof Boolean) {
+ valueString = Boolean.toString((Boolean) value);
+ arg = "--ez";
+ } else {
+ // treat is as a string.
+ valueString = value.toString();
+ arg = "--esmake";
+ }
+ parts.add(arg);
+ parts.add(valueString);
+ }
+
+ if (!isNullOrEmpty(component)) {
+ parts.add("-n");
+ parts.add(component);
+ }
+
+ if (flags != 0) {
+ parts.add("-f");
+ parts.add(Integer.toString(flags));
+ }
+
+ if (!isNullOrEmpty(uri)) {
+ parts.add(uri);
+ }
+
+ return parts;
+ }
+
+ @Override
+ protected Map<String, Object> instrument(String packageName, Map<String, Object> args) {
+ List<String> shellCmd = Lists.newArrayList("am", "instrument", "-w", "-r", packageName);
+ String result = shell(shellCmd.toArray(ZERO_LENGTH_STRING_ARRAY));
+ return convertInstrumentResult(result);
+ }
+
+ /**
+ * Convert the instrumentation result into it's Map representation.
+ *
+ * @param result the result string
+ * @return the new map
+ */
+ @VisibleForTesting
+ /* package */ static Map<String, Object> convertInstrumentResult(String result) {
+ Map<String, Object> map = Maps.newHashMap();
+ Pattern pattern = Pattern.compile("^INSTRUMENTATION_(\\w+): ", Pattern.MULTILINE);
+ Matcher matcher = pattern.matcher(result);
+
+ int previousEnd = 0;
+ String previousWhich = null;
+
+ while (matcher.find()) {
+ if ("RESULT".equals(previousWhich)) {
+ String resultLine = result.substring(previousEnd, matcher.start()).trim();
+ // Look for the = in the value, and split there
+ int splitIndex = resultLine.indexOf("=");
+ String key = resultLine.substring(0, splitIndex);
+ String value = resultLine.substring(splitIndex + 1);
+
+ map.put(key, value);
+ }
+
+ previousEnd = matcher.end();
+ previousWhich = matcher.group(1);
+ }
+ if ("RESULT".equals(previousWhich)) {
+ String resultLine = result.substring(previousEnd, matcher.start()).trim();
+ // Look for the = in the value, and split there
+ int splitIndex = resultLine.indexOf("=");
+ String key = resultLine.substring(0, splitIndex);
+ String value = resultLine.substring(splitIndex + 1);
+
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ @Override
+ protected void drag(int startx, int starty, int endx, int endy, int steps, long ms) {
+ final long iterationTime = ms / steps;
+
+ LinearInterpolator lerp = new LinearInterpolator(steps);
+ LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty);
+ LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy);
+ lerp.interpolate(start, end, new LinearInterpolator.Callback() {
+ public void step(Point point) {
+ try {
+ manager.touchMove(point.getX(), point.getY());
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending drag start event", e);
+ }
+
+ try {
+ Thread.sleep(iterationTime);
+ } catch (InterruptedException e) {
+ LOG.log(Level.SEVERE, "Error sleeping", e);
+ }
+ }
+
+ public void start(Point point) {
+ try {
+ manager.touchDown(point.getX(), point.getY());
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending drag start event", e);
+ }
+
+ try {
+ Thread.sleep(iterationTime);
+ } catch (InterruptedException e) {
+ LOG.log(Level.SEVERE, "Error sleeping", e);
+ }
+ }
+
+ public void end(Point point) {
+ try {
+ manager.touchUp(point.getX(), point.getY());
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error sending drag end event", e);
+ }
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.android.ddmlib.RawImage;
+import com.android.monkeyrunner.MonkeyImage;
+import com.android.monkeyrunner.adb.image.ImageUtils;
+
+import java.awt.image.BufferedImage;
+
+/**
+ * ADB implementation of the MonkeyImage class.
+ */
+public class AdbMonkeyImage extends MonkeyImage {
+ private final RawImage image;
+
+ /**
+ * Create a new AdbMonkeyImage.
+ *
+ * @param image the image from adb.
+ */
+ AdbMonkeyImage(RawImage image) {
+ this.image = image;
+ }
+
+ @Override
+ public BufferedImage createBufferedImage() {
+ return ImageUtils.convertImage(image);
+ }
+
+ public RawImage getRawImage() {
+ return image;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.android.ddmlib.IShellOutputReceiver;
+
+/**
+ * Shell Output Receiver that captures shell output into a String for
+ * later retrieval.
+ */
+public class CommandOutputCapture implements IShellOutputReceiver {
+ private final StringBuilder builder = new StringBuilder();
+
+ public void flush() { }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public void addOutput(byte[] data, int offset, int length) {
+ String message = new String(data, offset, length);
+ builder.append(message);
+ }
+
+ @Override
+ public String toString() {
+ return builder.toString();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+
+
+/**
+ * Linear Interpolation class.
+ */
+public class LinearInterpolator {
+ private final int steps;
+
+ /**
+ * Use our own Point class so we don't pull in java.awt.* just for this simple class.
+ */
+ public static class Point {
+ private final int x;
+ private final int y;
+
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder().
+ append("(").
+ append(x).
+ append(",").
+ append(y).
+ append(")").toString();
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Point) {
+ Point that = (Point) obj;
+ return this.x == that.x && this.y == that.y;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return 0x43125315 + x + y;
+ }
+
+ public int getX() {
+ return x;
+ }
+
+ public int getY() {
+ return y;
+ }
+ }
+
+ /**
+ * Callback interface to recieve interpolated points.
+ */
+ public interface Callback {
+ /**
+ * Called once to inform of the start point.
+ */
+ void start(Point point);
+ /**
+ * Called once to inform of the end point.
+ */
+ void end(Point point);
+ /**
+ * Called at every step in-between start and end.
+ */
+ void step(Point point);
+ }
+
+ /**
+ * Create a new linear Interpolator.
+ *
+ * @param steps How many steps should be in a single run. This counts the intervals
+ * in-between points, so the actual number of points generated will be steps + 1.
+ */
+ public LinearInterpolator(int steps) {
+ this.steps = steps;
+ }
+
+ // Copied from android.util.MathUtils since we couldn't link it in on the host.
+ private static float lerp(float start, float stop, float amount) {
+ return start + (stop - start) * amount;
+ }
+
+ /**
+ * Calculate the interpolated points.
+ *
+ * @param start The starting point
+ * @param end The ending point
+ * @param callback the callback to call with each calculated points.
+ */
+ public void interpolate(Point start, Point end, Callback callback) {
+ int xDistance = Math.abs(end.getX() - start.getX());
+ int yDistance = Math.abs(end.getY() - start.getY());
+ float amount = (float) (1.0 / steps);
+
+
+ callback.start(start);
+ for (int i = 1; i < steps; i++) {
+ float newX = lerp(start.getX(), end.getX(), amount * i);
+ float newY = lerp(start.getY(), end.getY(), amount * i);
+
+ callback.step(new Point(Math.round(newX), Math.round(newY)));
+ }
+ // Generate final point
+ callback.end(end);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.android.ddmlib.IShellOutputReceiver;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Shell Output Receiver that sends shell output to a Logger.
+ */
+public class LoggingOutputReceiver implements IShellOutputReceiver {
+ private final Logger log;
+ private final Level level;
+
+ public LoggingOutputReceiver(Logger log, Level level) {
+ this.log = log;
+ this.level = level;
+ }
+
+ public void addOutput(byte[] data, int offset, int length) {
+ String message = new String(data, offset, length);
+ for (String line : message.split("\n")) {
+ log.log(level, line);
+ }
+ }
+
+ public void flush() { }
+
+ public boolean isCancelled() {
+ return false;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb.image;
+
+import com.android.ddmlib.RawImage;
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.adb.AdbBackend;
+import com.android.monkeyrunner.adb.AdbMonkeyImage;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * Utility program to capture raw and converted images from a device and write them to a file.
+ * This is used to generate the test data for ImageUtilsTest.
+ */
+public class CaptureRawAndConvertedImage {
+ public static class MonkeyRunnerRawImage implements Serializable {
+ public int version;
+ public int bpp;
+ public int size;
+ public int width;
+ public int height;
+ public int red_offset;
+ public int red_length;
+ public int blue_offset;
+ public int blue_length;
+ public int green_offset;
+ public int green_length;
+ public int alpha_offset;
+ public int alpha_length;
+
+ public byte[] data;
+
+ public MonkeyRunnerRawImage(RawImage rawImage) {
+ version = rawImage.version;
+ bpp = rawImage.bpp;
+ size = rawImage.size;
+ width = rawImage.width;
+ height = rawImage.height;
+ red_offset = rawImage.red_offset;
+ red_length = rawImage.red_length;
+ blue_offset = rawImage.blue_offset;
+ blue_length = rawImage.blue_length;
+ green_offset = rawImage.green_offset;
+ green_length = rawImage.green_length;
+ alpha_offset = rawImage.alpha_offset;
+ alpha_length = rawImage.alpha_length;
+
+ data = rawImage.data;
+ }
+
+ public RawImage toRawImage() {
+ RawImage rawImage = new RawImage();
+
+ rawImage.version = version;
+ rawImage.bpp = bpp;
+ rawImage.size = size;
+ rawImage.width = width;
+ rawImage.height = height;
+ rawImage.red_offset = red_offset;
+ rawImage.red_length = red_length;
+ rawImage.blue_offset = blue_offset;
+ rawImage.blue_length = blue_length;
+ rawImage.green_offset = green_offset;
+ rawImage.green_length = green_length;
+ rawImage.alpha_offset = alpha_offset;
+ rawImage.alpha_length = alpha_length;
+
+ rawImage.data = data;
+ return rawImage;
+ }
+ }
+
+ private static void writeOutImage(RawImage screenshot, String name) throws IOException {
+ ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name));
+ out.writeObject(new MonkeyRunnerRawImage(screenshot));
+ out.close();
+ }
+
+ public static void main(String[] args) throws IOException {
+ AdbBackend backend = new AdbBackend();
+ MonkeyDevice device = backend.waitForConnection();
+ AdbMonkeyImage snapshot = (AdbMonkeyImage) device.takeSnapshot();
+
+ // write out to a file
+ snapshot.writeToFile("output.png", "png");
+ writeOutImage(snapshot.getRawImage(), "output.raw");
+ System.exit(0);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Point;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.util.Hashtable;
+/**
+ * Useful image related functions.
+ */
+public class ImageUtils {
+ // Utility class
+ private ImageUtils() { }
+
+ private static Hashtable<?,?> EMPTY_HASH = new Hashtable();
+ private static int[] BAND_OFFSETS_32 = { 0, 1, 2, 3 };
+ private static int[] BAND_OFFSETS_16 = { 0, 1 };
+
+ /**
+ * Convert a raw image into a buffered image.
+ *
+ * @param rawImage the raw image to convert
+ * @param image the old image to (possibly) recycle
+ * @return the converted image
+ */
+ public static BufferedImage convertImage(RawImage rawImage, BufferedImage image) {
+ switch (rawImage.bpp) {
+ case 16:
+ return rawImage16toARGB(image, rawImage);
+ case 32:
+ return rawImage32toARGB(rawImage);
+ }
+ return null;
+ }
+
+ /**
+ * Convert a raw image into a buffered image.
+ *
+ * @param rawImage the image to convert.
+ * @return the converted image.
+ */
+ public static BufferedImage convertImage(RawImage rawImage) {
+ return convertImage(rawImage, null);
+ }
+
+ static int getMask(int length) {
+ int res = 0;
+ for (int i = 0 ; i < length ; i++) {
+ res = (res << 1) + 1;
+ }
+
+ return res;
+ }
+
+ private static BufferedImage rawImage32toARGB(RawImage rawImage) {
+ // Do as much as we can to not make an extra copy of the data. This is just a bunch of
+ // classes that wrap's the raw byte array of the image data.
+ DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size);
+
+ PixelInterleavedSampleModel sampleModel =
+ new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height,
+ 4, rawImage.width * 4, BAND_OFFSETS_32);
+ WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer,
+ new Point(0, 0));
+ return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, false, EMPTY_HASH);
+ }
+
+ private static BufferedImage rawImage16toARGB(BufferedImage image, RawImage rawImage) {
+ // Do as much as we can to not make an extra copy of the data. This is just a bunch of
+ // classes that wrap's the raw byte array of the image data.
+ DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, rawImage.size);
+
+ PixelInterleavedSampleModel sampleModel =
+ new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, rawImage.width, rawImage.height,
+ 2, rawImage.width * 2, BAND_OFFSETS_16);
+ WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuffer,
+ new Point(0, 0));
+ return new BufferedImage(new SixteenBitColorModel(rawImage), raster, false, EMPTY_HASH);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+
+/**
+ * Internal color model used to do conversion of 16bpp RawImages.
+ */
+class SixteenBitColorModel extends ColorModel {
+ private static final int[] BITS = {
+ 8, 8, 8, 8
+ };
+ public SixteenBitColorModel(RawImage rawImage) {
+ super(32
+ , BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ true, false, Transparency.TRANSLUCENT,
+ DataBuffer.TYPE_BYTE);
+ }
+
+ @Override
+ public boolean isCompatibleRaster(Raster raster) {
+ return true;
+ }
+
+ private int getPixel(Object inData) {
+ byte[] data = (byte[]) inData;
+ int value = data[0] & 0x00FF;
+ value |= (data[1] << 8) & 0x0FF00;
+
+ return value;
+ }
+
+ @Override
+ public int getAlpha(Object inData) {
+ return 0xff;
+ }
+
+ @Override
+ public int getBlue(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >> 0) & 0x01F) << 3;
+ }
+
+ @Override
+ public int getGreen(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >> 5) & 0x03F) << 2;
+ }
+
+ @Override
+ public int getRed(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >> 11) & 0x01F) << 3;
+ }
+
+ @Override
+ public int getAlpha(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getBlue(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getGreen(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRed(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb.image;
+
+import com.android.ddmlib.RawImage;
+
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.Raster;
+
+/**
+ * Internal color model used to do conversion of 32bpp RawImages.
+ */
+class ThirtyTwoBitColorModel extends ColorModel {
+ private static final int[] BITS = {
+ 8, 8, 8, 8,
+ };
+ private final int alphaLength;
+ private final int alphaMask;
+ private final int alphaOffset;
+ private final int blueMask;
+ private final int blueLength;
+ private final int blueOffset;
+ private final int greenMask;
+ private final int greenLength;
+ private final int greenOffset;
+ private final int redMask;
+ private final int redLength;
+ private final int redOffset;
+
+ public ThirtyTwoBitColorModel(RawImage rawImage) {
+ super(32, BITS, ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ true, false, Transparency.TRANSLUCENT,
+ DataBuffer.TYPE_BYTE);
+
+ redOffset = rawImage.red_offset;
+ redLength = rawImage.red_length;
+ redMask = ImageUtils.getMask(redLength);
+ greenOffset = rawImage.green_offset;
+ greenLength = rawImage.green_length;
+ greenMask = ImageUtils.getMask(greenLength);
+ blueOffset = rawImage.blue_offset;
+ blueLength = rawImage.blue_length;
+ blueMask = ImageUtils.getMask(blueLength);
+ alphaLength = rawImage.alpha_length;
+ alphaOffset = rawImage.alpha_offset;
+ alphaMask = ImageUtils.getMask(alphaLength);
+ }
+
+ @Override
+ public boolean isCompatibleRaster(Raster raster) {
+ return true;
+ }
+
+ private int getPixel(Object inData) {
+ byte[] data = (byte[]) inData;
+ int value = data[0] & 0x00FF;
+ value |= (data[1] & 0x00FF) << 8;
+ value |= (data[2] & 0x00FF) << 16;
+ value |= (data[3] & 0x00FF) << 24;
+
+ return value;
+ }
+
+ @Override
+ public int getAlpha(Object inData) {
+ int pixel = getPixel(inData);
+ if(alphaLength == 0) {
+ return 0xff;
+ }
+ return ((pixel >>> alphaOffset) & alphaMask) << (8 - alphaLength);
+ }
+
+ @Override
+ public int getBlue(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >>> blueOffset) & blueMask) << (8 - blueLength);
+ }
+
+ @Override
+ public int getGreen(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >>> greenOffset) & greenMask) << (8 - greenLength);
+ }
+
+ @Override
+ public int getRed(Object inData) {
+ int pixel = getPixel(inData);
+ return ((pixel >>> redOffset) & redMask) << (8 - redLength);
+ }
+
+ @Override
+ public int getAlpha(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getBlue(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getGreen(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getRed(int pixel) {
+ throw new UnsupportedOperationException();
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.controller;
+
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.adb.AdbBackend;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.logging.Logger;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+/**
+ * Application that can control an attached device using the network monkey. It has a window
+ * that shows what the current screen looks like and allows the user to click in it. Clicking in
+ * the window sends touch events to the attached device. It also supports keyboard input for
+ * typing and has buttons to press to simulate physical buttons on the device.
+ */
+public class MonkeyController extends JFrame {
+ private static final Logger LOG = Logger.getLogger(MonkeyController.class.getName());
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ AdbBackend adb = new AdbBackend();
+ final MonkeyDevice device = adb.waitForConnection();
+ MonkeyControllerFrame mf = new MonkeyControllerFrame(device);
+ mf.setVisible(true);
+ mf.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosed(WindowEvent e) {
+ device.dispose();
+ }
+ });
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.controller;
+
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.MonkeyImage;
+import com.android.monkeyrunner.MonkeyManager;
+import com.android.monkeyrunner.PhysicalButton;
+
+import java.awt.KeyEventDispatcher;
+import java.awt.KeyboardFocusManager;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.swing.AbstractAction;
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JToolBar;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+
+/**
+ * Main window for MonkeyController.
+ */
+public class MonkeyControllerFrame extends JFrame {
+ private static final Logger LOG = Logger.getLogger(MonkeyControllerFrame.class.getName());
+
+ private final JButton refreshButton = new JButton("Refresh");
+ private final JButton variablesButton = new JButton("Variable");
+ private final JLabel imageLabel = new JLabel();
+ private final VariableFrame variableFrame;
+
+ private MonkeyManager monkeyManager;
+ private BufferedImage currentImage;
+
+ private final Timer timer = new Timer(1000, new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ updateScreen();
+ }
+ });
+
+ private final MonkeyDevice device;
+
+ private class PressAction extends AbstractAction {
+ private final PhysicalButton button;
+
+ public PressAction(PhysicalButton button) {
+ this.button = button;
+ }
+ public void actionPerformed(ActionEvent event) {
+ try {
+ monkeyManager.press(button);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ updateScreen();
+ }
+ }
+
+ private JButton createToolbarButton(PhysicalButton hardButton) {
+ JButton button = new JButton(new PressAction(hardButton));
+ button.setText(hardButton.getKeyName());
+ return button;
+ }
+
+ public MonkeyControllerFrame(MonkeyDevice device) {
+ super("MonkeyController");
+ this.device = device;
+
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+ setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
+ JToolBar toolbar = new JToolBar();
+
+ toolbar.add(createToolbarButton(PhysicalButton.HOME));
+ toolbar.add(createToolbarButton(PhysicalButton.BACK));
+ toolbar.add(createToolbarButton(PhysicalButton.SEARCH));
+ toolbar.add(createToolbarButton(PhysicalButton.MENU));
+
+ add(toolbar);
+ add(refreshButton);
+ add(variablesButton);
+ add(imageLabel);
+
+ refreshButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ updateScreen();
+ }
+ });
+
+ variableFrame = new VariableFrame();
+ variablesButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ variableFrame.setVisible(true);
+ }
+ });
+
+ imageLabel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ try {
+ monkeyManager.touch(event.getX(), event.getY());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ updateScreen();
+ }
+
+ });
+
+ KeyboardFocusManager focusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
+ focusManager.addKeyEventDispatcher(new KeyEventDispatcher() {
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (KeyEvent.KEY_TYPED == event.getID()) {
+ try {
+ monkeyManager.type(event.getKeyChar());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ }
+ });
+
+ SwingUtilities.invokeLater(new Runnable(){
+ public void run() {
+ init();
+ variableFrame.init(monkeyManager);
+ }
+ });
+
+ pack();
+ }
+
+ private void updateScreen() {
+ MonkeyImage snapshot = device.takeSnapshot();
+ currentImage = snapshot.createBufferedImage();
+ imageLabel.setIcon(new ImageIcon(currentImage));
+
+ pack();
+ }
+
+ private void init() {
+ monkeyManager = device.getManager();
+ if (monkeyManager == null) {
+ throw new RuntimeException("Unable to create monkey manager");
+ }
+ updateScreen();
+ timer.start();
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.controller;
+
+import com.google.common.collect.Sets;
+
+import com.android.monkeyrunner.MonkeyManager;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
+import javax.swing.WindowConstants;
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * Swing Frame that displays all the variables that the monkey exposes on the device.
+ */
+public class VariableFrame extends JFrame {
+ private static final Logger LOG = Logger.getLogger(VariableFrame.class.getName());
+ private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
+ private MonkeyManager monkeyManager;
+
+ private static class VariableHolder implements Comparable<VariableHolder> {
+ private final String key;
+ private final String value;
+
+ public VariableHolder(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public int compareTo(VariableHolder o) {
+ return key.compareTo(o.key);
+ }
+ }
+
+ private static <E> E getNthElement(Set<E> set, int offset) {
+ int current = 0;
+ for (E elem : set) {
+ if (current == offset) {
+ return elem;
+ }
+ current++;
+ }
+ return null;
+ }
+
+ private class VariableTableModel extends AbstractTableModel {
+ private final TreeSet<VariableHolder> set = Sets.newTreeSet();
+
+ public void refresh() {
+ Collection<String> variables;
+ try {
+ variables = monkeyManager.listVariable();
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE, "Error getting list of variables", e);
+ return;
+ }
+ for (final String variable : variables) {
+ EXECUTOR.execute(new Runnable() {
+ public void run() {
+ String value;
+ try {
+ value = monkeyManager.getVariable(variable);
+ } catch (IOException e) {
+ LOG.log(Level.SEVERE,
+ "Error getting variable value for " + variable, e);
+ return;
+ }
+ if (value == null) {
+ value = "";
+ }
+ synchronized (set) {
+ set.add(new VariableHolder(variable, value));
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ VariableTableModel.this.fireTableDataChanged();
+ }
+ });
+
+ }
+ }
+ });
+ }
+ }
+
+ public int getColumnCount() {
+ return 2;
+ }
+
+ public int getRowCount() {
+ synchronized (set) {
+ return set.size();
+ }
+ }
+
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ VariableHolder nthElement;
+ synchronized (set) {
+ nthElement = getNthElement(set, rowIndex);
+ }
+ if (columnIndex == 0) {
+ return nthElement.getKey();
+ }
+ return nthElement.getValue();
+ }
+ }
+
+ public VariableFrame() {
+ super("Variables");
+ setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
+ setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
+
+ final VariableTableModel tableModel = new VariableTableModel();
+
+ JButton refreshButton = new JButton("Refresh");
+ add(refreshButton);
+ refreshButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ tableModel.refresh();
+ }
+ });
+
+
+ JTable table = new JTable(tableModel);
+ add(table);
+
+ tableModel.addTableModelListener(new TableModelListener() {
+ public void tableChanged(TableModelEvent e) {
+ pack();
+ }
+ });
+
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowOpened(WindowEvent e) {
+ tableModel.refresh();
+ }
+ });
+
+ pack();
+ }
+
+ public void init(MonkeyManager monkeyManager) {
+ this.monkeyManager = monkeyManager;
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.doc;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the annotated method is a public API to expose to the
+ * scripting interface. Can be used to generate documentation of what
+ * methods are exposed and also can be used to enforce visibility of
+ * these methods in the scripting environment.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE })
+public @interface MonkeyRunnerExported {
+ /**
+ * A documentation string for this method.
+ */
+ String doc();
+
+ /**
+ * The list of names for the keywords in this method in their proper positional order.
+ *
+ * For example:
+ *
+ * @MonkeyRunnerExported(args={"one", "two"})
+ * public void foo();
+ *
+ * would allow calls like this:
+ * foo(one=1, two=2)
+ * foo(1, 2)
+ */
+ String[] args() default {};
+
+ /**
+ * The list of documentation for the arguments.
+ */
+ String[] argDocs() default {};
+
+ /**
+ * The documentation for the return type of this method.
+ */
+ String returns() default "returns nothing.";
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.exceptions;
+
+/**
+ * Base exception class for all MonkeyRunner Exceptions.
+ */
+public class MonkeyRunnerException extends Exception {
+ public MonkeyRunnerException(String message) {
+ super(message);
+ }
+
+ public MonkeyRunnerException(Throwable e) {
+ super(e);
+ }
+
+ public MonkeyRunnerException(String message, Throwable e) {
+ super(message, e);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.stub;
+
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.MonkeyManager;
+import com.android.monkeyrunner.MonkeyRunnerBackend;
+
+public class StubBackend implements MonkeyRunnerBackend {
+
+ public MonkeyManager createManager(String address, int port) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public MonkeyDevice waitForConnection(long timeout, String deviceId) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public void shutdown() {
+ // We're stub - we've got nothing to do.
+ }
+}
--- /dev/null
+<html>
+<body>
+<h1>MonkeyRunner Help<h1>
+<h2>Table of Contents</h2>
+<ul>
+<?cs each:item = help ?>
+<li><a href="#<?cs name:item ?>"><?cs var:item.name ?></a></li>
+<?cs /each ?>
+</ul>
+<?cs each:item = help ?>
+<h2><a name="<?cs name:item ?>"><?cs var:item.name ?></a></h2>
+ <p><?cs var:item.doc ?></p>
+ <?cs if:subcount(item.argument) ?>
+<h3>Args</h3>
+<ul>
+ <?cs each:arg = item.argument ?>
+ <li><?cs var:arg.name ?> - <?cs var:arg.doc ?></li>
+ <?cs /each ?>
+</ul>
+<h3>Returns</h3>
+<p><?cs var:item.returns ?></p>
+<?cs /if ?>
+<?cs /each ?>
+</body>
+</html>
--- /dev/null
+MonkeyRunner help
+<?cs each:item = help ?>
+<?cs var:item.name ?>
+ <?cs var:item.doc ?>
+
+<?cs if:subcount(item.argument) ?> Args:<?cs each:arg = item.argument ?>
+ <?cs var:arg.name ?> - <?cs var:arg.doc ?><?cs /each ?>
+<?cs /if ?> Returns: <?cs var:item.returns ?>
+<?cs /each ?>
--- /dev/null
+#
+# Copyright (C) 2010 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-subdir-java-files)
+LOCAL_MODULE := MonkeyRunnerTest
+LOCAL_JAVA_LIBRARIES := junit monkeyrunner ddmlib guavalib jython
+
+include $(BUILD_HOST_JAVA_LIBRARY)
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.android.monkeyrunner.adb.AdbMonkeyDeviceTest;
+import com.android.monkeyrunner.adb.LinearInterpolatorTest;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+import junit.textui.TestRunner;
+
+/**
+ * Test suite to run all the tests for MonkeyRunner.
+ */
+public class AllTests {
+ public static Test suite(Class<? extends TestCase>... classes) {
+ TestSuite suite = new TestSuite();
+ for (Class<? extends TestCase> clz : classes) {
+ suite.addTestSuite(clz);
+ }
+ return suite;
+ }
+
+ public static void main(String args[]) {
+ TestRunner tr = new TestRunner();
+ TestResult result = tr.doRun(AllTests.suite(ImageUtilsTest.class, JythonUtilsTest.class,
+ MonkeyRunnerOptionsTest.class, LinearInterpolatorTest.class,
+ AdbMonkeyDeviceTest.class));
+ if (result.wasSuccessful()) {
+ System.exit(0);
+ } else {
+ System.exit(1);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.android.ddmlib.RawImage;
+import com.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage;
+import com.android.monkeyrunner.adb.image.ImageUtils;
+import com.android.monkeyrunner.adb.image.CaptureRawAndConvertedImage.MonkeyRunnerRawImage;
+
+import junit.framework.TestCase;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+
+import javax.imageio.ImageIO;
+
+public class ImageUtilsTest extends TestCase {
+ private static BufferedImage createBufferedImage(String name) throws IOException {
+ InputStream is = ImageUtilsTest.class.getResourceAsStream(name);
+ BufferedImage img = ImageIO.read(is);
+ is.close();
+ return img;
+ }
+
+ private static RawImage createRawImage(String name) throws IOException, ClassNotFoundException {
+ ObjectInputStream is =
+ new ObjectInputStream(ImageUtilsTest.class.getResourceAsStream(name));
+ CaptureRawAndConvertedImage.MonkeyRunnerRawImage wrapper = (MonkeyRunnerRawImage) is.readObject();
+ is.close();
+ return wrapper.toRawImage();
+ }
+
+ /**
+ * Check that the two images will draw the same (ie. have the same pixels). This is different
+ * that BufferedImage.equals(), which also wants to check that they have the same ColorModel
+ * and other parameters.
+ *
+ * @param i1 the first image
+ * @param i2 the second image
+ * @return true if both images will draw the same (ie. have same pixels).
+ */
+ private static boolean checkImagesHaveSamePixels(BufferedImage i1, BufferedImage i2) {
+ if (i1.getWidth() != i2.getWidth()) {
+ return false;
+ }
+ if (i1.getHeight() != i2.getHeight()) {
+ return false;
+ }
+
+ for (int y = 0; y < i1.getHeight(); y++) {
+ for (int x = 0; x < i1.getWidth(); x++) {
+ int p1 = i1.getRGB(x, y);
+ int p2 = i2.getRGB(x, y);
+ if (p1 != p2) {
+ WritableRaster r1 = i1.getRaster();
+ WritableRaster r2 = i2.getRaster();
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public void testImageConversionOld() throws IOException, ClassNotFoundException {
+ RawImage rawImage = createRawImage("image1.raw");
+ BufferedImage convertedImage = ImageUtils.convertImage(rawImage);
+ BufferedImage correctConvertedImage = createBufferedImage("image1.png");
+
+ assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage));
+ }
+
+ public void testImageConversionNew() throws IOException, ClassNotFoundException {
+ RawImage rawImage = createRawImage("image2.raw");
+ BufferedImage convertedImage = ImageUtils.convertImage(rawImage);
+ BufferedImage correctConvertedImage = createBufferedImage("image2.png");
+
+ assertTrue(checkImagesHaveSamePixels(convertedImage, correctConvertedImage));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import junit.framework.TestCase;
+
+import org.python.core.ArgParser;
+import org.python.core.PyDictionary;
+import org.python.core.PyException;
+import org.python.core.PyObject;
+import org.python.core.PyString;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for the JythonUtils class.
+ */
+public class JythonUtilsTest extends TestCase {
+ private static final String PACKAGE_NAME = JythonUtilsTest.class.getPackage().getName();
+ private static final String CLASS_NAME = JythonUtilsTest.class.getSimpleName();
+
+ private static boolean called = false;
+ private static double floatValue = 0.0;
+ private static List<Object> listValue = null;
+ private static Map<String, Object> mapValue;
+
+ @MonkeyRunnerExported(doc = "", args = {"value"})
+ public static void floatTest(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+ called = true;
+
+ floatValue = JythonUtils.getFloat(ap, 0);
+ }
+
+ @MonkeyRunnerExported(doc = "", args = {"value"})
+ public static void listTest(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+ called = true;
+
+ listValue = JythonUtils.getList(ap, 0);
+ }
+
+ @MonkeyRunnerExported(doc = "", args = {"value"})
+ public static void mapTest(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+ called = true;
+
+ mapValue = JythonUtils.getMap(ap, 0);
+ }
+
+ @MonkeyRunnerExported(doc = "")
+ public static PyDictionary convertMapTest(PyObject[] args, String[] kws) {
+ Map<String, Object> map = Maps.newHashMap();
+ map.put("string", "value");
+ map.put("integer", 1);
+ map.put("double", 3.14);
+ return JythonUtils.convertMapToDict(map);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ called = false;
+ floatValue = 0.0;
+ }
+
+ private static PyObject call(String method) {
+ return call(method, new String[]{ });
+ }
+ private static PyObject call(String method, String... args) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("from ").append(PACKAGE_NAME);
+ sb.append(" import ").append(CLASS_NAME).append("\n");
+
+ // Exec line
+ sb.append("result = ");
+ sb.append(CLASS_NAME).append(".").append(method);
+ sb.append("(");
+ for (String arg : args) {
+ sb.append(arg).append(",");
+ }
+ sb.append(")");
+
+ return ScriptRunner.runStringAndGet(sb.toString(), "result").get("result");
+ }
+
+ public void testSimpleCall() {
+ call("floatTest", "0.0");
+ assertTrue(called);
+ }
+
+ public void testMissingFloatArg() {
+ try {
+ call("floatTest");
+ } catch(PyException e) {
+ return;
+ }
+ fail("Should have thrown exception");
+ }
+
+ public void testBadFloatArgType() {
+ try {
+ call("floatTest", "\'foo\'");
+ } catch(PyException e) {
+ return;
+ }
+ fail("Should have thrown exception");
+ }
+
+ public void testFloatParse() {
+ call("floatTest", "103.2");
+ assertTrue(called);
+ assertEquals(floatValue, 103.2);
+ }
+
+ public void testFloatParseInteger() {
+ call("floatTest", "103");
+ assertTrue(called);
+ assertEquals(floatValue, 103.0);
+ }
+
+ public void testParseStringList() {
+ call("listTest", "['a', 'b', 'c']");
+ assertTrue(called);
+ assertEquals(3, listValue.size());
+ assertEquals("a", listValue.get(0));
+ assertEquals("b", listValue.get(1));
+ assertEquals("c", listValue.get(2));
+ }
+
+ public void testParseIntList() {
+ call("listTest", "[1, 2, 3]");
+ assertTrue(called);
+ assertEquals(3, listValue.size());
+ assertEquals(new Integer(1), listValue.get(0));
+ assertEquals(new Integer(2), listValue.get(1));
+ assertEquals(new Integer(3), listValue.get(2));
+ }
+
+ public void testParseMixedList() {
+ call("listTest", "['a', 1, 3.14]");
+ assertTrue(called);
+ assertEquals(3, listValue.size());
+ assertEquals("a", listValue.get(0));
+ assertEquals(new Integer(1), listValue.get(1));
+ assertEquals(new Double(3.14), listValue.get(2));
+ }
+
+ public void testParseOptionalList() {
+ call("listTest");
+ assertTrue(called);
+ assertEquals(0, listValue.size());
+ }
+
+ public void testParsingNotAList() {
+ try {
+ call("listTest", "1.0");
+ } catch (PyException e) {
+ return;
+ }
+ fail("Should have thrown an exception");
+ }
+
+ public void testParseMap() {
+ call("mapTest", "{'a': 0, 'b': 'bee', 3: 'cee'}");
+ assertTrue(called);
+ assertEquals(3, mapValue.size());
+ assertEquals(new Integer(0), mapValue.get("a"));
+ assertEquals("bee", mapValue.get("b"));
+ // note: coerced key type
+ assertEquals("cee", mapValue.get("3"));
+ }
+
+ public void testParsingNotAMap() {
+ try {
+ call("mapTest", "1.0");
+ } catch (PyException e) {
+ return;
+ }
+ fail("Should have thrown an exception");
+ }
+
+ public void testParseOptionalMap() {
+ call("mapTest");
+ assertTrue(called);
+ assertEquals(0, mapValue.size());
+ }
+
+ public void testConvertMap() {
+ PyDictionary result = (PyDictionary) call("convertMapTest");
+ PyObject stringPyObject = result.__getitem__(new PyString("string"));
+ String string = (String) stringPyObject.__tojava__(String.class);
+ assertEquals("value", string);
+
+ PyObject intPyObject = result.__getitem__(new PyString("integer"));
+ int i = (Integer) intPyObject.__tojava__(Integer.class);
+ assertEquals(i, 1);
+
+ PyObject doublePyObject = result.__getitem__(new PyString("double"));
+ double d = (Double) doublePyObject.__tojava__(Double.class);
+ assertEquals(3.14, d);
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.util.Iterator;
+
+/**
+ * Unit Tests to test command line argument parsing.
+ */
+public class MonkeyRunnerOptionsTest extends TestCase {
+ // We need to use a file that actually exists
+ private static final String FILENAME = "/etc/passwd";
+
+ public void testSimpleArgs() {
+ MonkeyRunnerOptions options =
+ MonkeyRunnerOptions.processOptions(new String[] { FILENAME });
+ assertEquals(options.getScriptFile(), new File(FILENAME));
+ }
+
+ public void testParsingArgsBeforeScriptName() {
+ MonkeyRunnerOptions options =
+ MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME});
+ assertEquals("stub", options.getBackendName());
+ assertEquals(options.getScriptFile(), new File(FILENAME));
+ }
+
+ public void testParsingScriptArgument() {
+ MonkeyRunnerOptions options =
+ MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "arg1", "arg2" });
+ assertEquals(options.getScriptFile(), new File(FILENAME));
+ Iterator<String> i = options.getArguments().iterator();
+ assertEquals("arg1", i.next());
+ assertEquals("arg2", i.next());
+ }
+
+ public void testParsingScriptArgumentWithDashes() {
+ MonkeyRunnerOptions options =
+ MonkeyRunnerOptions.processOptions(new String[] { FILENAME, "--arg1" });
+ assertEquals(options.getScriptFile(), new File(FILENAME));
+ assertEquals("--arg1", options.getArguments().iterator().next());
+ }
+
+ public void testMixedArgs() {
+ MonkeyRunnerOptions options =
+ MonkeyRunnerOptions.processOptions(new String[] { "-be", "stub", FILENAME,
+ "arg1", "--debug=True"});
+ assertEquals("stub", options.getBackendName());
+ assertEquals(options.getScriptFile(), new File(FILENAME));
+ Iterator<String> i = options.getArguments().iterator();
+ assertEquals("arg1", i.next());
+ assertEquals("--debug=True", i.next());
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.google.common.base.Joiner;
+import com.google.common.io.Resources;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit Tests for AdbMonkeyDevice.
+ */
+public class AdbMonkeyDeviceTest extends TestCase {
+ private static String MULTILINE_RESULT = "\r\n" +
+ "Test results for InstrumentationTestRunner=.\r\n" +
+ "Time: 2.242\r\n" +
+ "\r\n" +
+ "OK (1 test)";
+
+ private static String getResource(String resName) throws IOException {
+ URL resource = Resources.getResource(AdbMonkeyDeviceTest.class, resName);
+ List<String> lines = Resources.readLines(resource, Charset.defaultCharset());
+ return Joiner.on("\r\n").join(lines);
+ }
+
+ public void testSimpleResultParse() throws IOException {
+ String result = getResource("instrument_result.txt");
+ Map<String, Object> convertedResult = AdbMonkeyDevice.convertInstrumentResult(result);
+
+ assertEquals("one", convertedResult.get("result1"));
+ assertEquals("two", convertedResult.get("result2"));
+ }
+
+ public void testMultilineResultParse() throws IOException {
+ String result = getResource("multiline_instrument_result.txt");
+ Map<String, Object> convertedResult = AdbMonkeyDevice.convertInstrumentResult(result);
+
+ assertEquals(MULTILINE_RESULT, convertedResult.get("stream"));
+ }
+}
--- /dev/null
+/*
+ * Copyright (C) 2010 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.monkeyrunner.adb;
+
+import com.google.common.collect.Lists;
+
+import com.android.monkeyrunner.adb.LinearInterpolator.Point;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Unit tests for the LinerInterpolator class.S
+ */
+public class LinearInterpolatorTest extends TestCase {
+ private static class Collector implements LinearInterpolator.Callback {
+ private final List<LinearInterpolator.Point> points = Lists.newArrayList();
+
+ public List<LinearInterpolator.Point> getPoints() {
+ return points;
+ }
+
+ public void end(Point input) {
+ points.add(input);
+ }
+
+ public void start(Point input) {
+ points.add(input);
+ }
+
+ public void step(Point input) {
+ points.add(input);
+ }
+ }
+
+ List<Integer> STEP_POINTS = Lists.newArrayList(0, 100, 200, 300, 400, 500, 600, 700, 800, 900,
+ 1000);
+ List<Integer> REVERSE_STEP_POINTS = Lists.newArrayList(1000, 900, 800, 700, 600, 500, 400, 300,
+ 200, 100, 0);
+
+ public void testLerpRight() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(0, 100),
+ new LinearInterpolator.Point(1000, 100),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(STEP_POINTS.get(x), 100), points.get(x));
+ }
+ }
+
+ public void testLerpLeft() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(1000, 100),
+ new LinearInterpolator.Point(0, 100),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(REVERSE_STEP_POINTS.get(x), 100), points.get(x));
+ }
+ }
+
+ public void testLerpUp() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(100, 1000),
+ new LinearInterpolator.Point(100, 0),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(100, REVERSE_STEP_POINTS.get(x)), points.get(x));
+ }
+ }
+
+ public void testLerpDown() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(100, 0),
+ new LinearInterpolator.Point(100, 1000),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(100, STEP_POINTS.get(x)), points.get(x));
+ }
+ }
+
+ public void testLerpNW() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(0, 0),
+ new LinearInterpolator.Point(1000, 1000),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(STEP_POINTS.get(x), STEP_POINTS.get(x)), points.get(x));
+ }
+ }
+
+ public void testLerpNE() {
+ LinearInterpolator lerp = new LinearInterpolator(10);
+ Collector collector = new Collector();
+ lerp.interpolate(new LinearInterpolator.Point(1000, 1000),
+ new LinearInterpolator.Point(0, 0),
+ collector);
+
+ List<LinearInterpolator.Point> points = collector.getPoints();
+ assertEquals(11, points.size());
+ for (int x = 0; x < points.size(); x++) {
+ assertEquals(new Point(REVERSE_STEP_POINTS.get(x), REVERSE_STEP_POINTS.get(x)), points.get(x));
+ }
+ }
+}
--- /dev/null
+INSTRUMENTATION_STATUS: id=InstrumentationTestRunner
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: result1=one
+INSTRUMENTATION_RESULT: result2=two
+INSTRUMENTATION_CODE: -1
--- /dev/null
+INSTRUMENTATION_STATUS: id=InstrumentationTestRunner
+INSTRUMENTATION_STATUS: current=1
+INSTRUMENTATION_STATUS: class=com.example.android.notepad.NotePadTest
+INSTRUMENTATION_STATUS: stream=.
+INSTRUMENTATION_STATUS: numtests=1
+INSTRUMENTATION_STATUS: test=testActivityTestCaseSetUpProperly
+INSTRUMENTATION_STATUS_CODE: 0
+INSTRUMENTATION_RESULT: stream=
+Test results for InstrumentationTestRunner=.
+Time: 2.242
+
+OK (1 test)
+
+
+INSTRUMENTATION_CODE: -1