OSDN Git Service

Release MonkeyRunner into open source.
authorBill Napier <napier@google.com>
Tue, 27 Jul 2010 23:18:34 +0000 (16:18 -0700)
committerBill Napier <napier@google.com>
Tue, 27 Jul 2010 23:40:25 +0000 (16:40 -0700)
Change-Id: Ie08e493e700e3e4c85270629f68547a08b7457d4

53 files changed:
monkeyrunner/Android.mk [new file with mode: 0644]
monkeyrunner/MODULE_LICENSE_APACHE2 [new file with mode: 0644]
monkeyrunner/NOTICE [new file with mode: 0644]
monkeyrunner/etc/Android.mk [new file with mode: 0644]
monkeyrunner/etc/manifest.txt [new file with mode: 0644]
monkeyrunner/etc/monkeyrunner [new file with mode: 0755]
monkeyrunner/jython/test/MonkeyRunner_test.py [new file with mode: 0644]
monkeyrunner/jython/test/all_tests.py [new file with mode: 0644]
monkeyrunner/scripts/help.py [new file with mode: 0644]
monkeyrunner/src/Android.mk [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java [new file with mode: 0644]
monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java [new file with mode: 0644]
monkeyrunner/src/resources/com/android/monkeyrunner/html.cs [new file with mode: 0644]
monkeyrunner/src/resources/com/android/monkeyrunner/text.cs [new file with mode: 0644]
monkeyrunner/test/Android.mk [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/AllTests.java [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java [new file with mode: 0644]
monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/image1.png [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/image2.png [new file with mode: 0644]
monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw [new file with mode: 0644]

diff --git a/monkeyrunner/Android.mk b/monkeyrunner/Android.mk
new file mode 100644 (file)
index 0000000..21cf67a
--- /dev/null
@@ -0,0 +1,19 @@
+#
+# 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
diff --git a/monkeyrunner/MODULE_LICENSE_APACHE2 b/monkeyrunner/MODULE_LICENSE_APACHE2
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/monkeyrunner/NOTICE b/monkeyrunner/NOTICE
new file mode 100644 (file)
index 0000000..c5b1efa
--- /dev/null
@@ -0,0 +1,190 @@
+
+   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
+
diff --git a/monkeyrunner/etc/Android.mk b/monkeyrunner/etc/Android.mk
new file mode 100644 (file)
index 0000000..2d757fd
--- /dev/null
@@ -0,0 +1,20 @@
+#
+# 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)
diff --git a/monkeyrunner/etc/manifest.txt b/monkeyrunner/etc/manifest.txt
new file mode 100644 (file)
index 0000000..706842e
--- /dev/null
@@ -0,0 +1 @@
+Main-Class: com.android.monkeyrunner.MonkeyRunnerStarter
diff --git a/monkeyrunner/etc/monkeyrunner b/monkeyrunner/etc/monkeyrunner
new file mode 100755 (executable)
index 0000000..364be2a
--- /dev/null
@@ -0,0 +1,74 @@
+#!/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" "$@"
diff --git a/monkeyrunner/jython/test/MonkeyRunner_test.py b/monkeyrunner/jython/test/MonkeyRunner_test.py
new file mode 100644 (file)
index 0000000..cc4d1f2
--- /dev/null
@@ -0,0 +1,54 @@
+#!/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()
diff --git a/monkeyrunner/jython/test/all_tests.py b/monkeyrunner/jython/test/all_tests.py
new file mode 100644 (file)
index 0000000..2dd0ab4
--- /dev/null
@@ -0,0 +1,49 @@
+#!/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)
diff --git a/monkeyrunner/scripts/help.py b/monkeyrunner/scripts/help.py
new file mode 100644 (file)
index 0000000..832d2cb
--- /dev/null
@@ -0,0 +1,48 @@
+#!/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)
diff --git a/monkeyrunner/src/Android.mk b/monkeyrunner/src/Android.mk
new file mode 100644 (file)
index 0000000..59337c6
--- /dev/null
@@ -0,0 +1,32 @@
+#
+# 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)
diff --git a/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java b/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java
new file mode 100644 (file)
index 0000000..258261b
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java
new file mode 100644 (file)
index 0000000..87c54c2
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+ * 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();
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyFormatter.java
new file mode 100644 (file)
index 0000000..c4a5362
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java
new file mode 100644 (file)
index 0000000..7cff67f
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+ * 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
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyManager.java
new file mode 100644 (file)
index 0000000..11a2dd4
--- /dev/null
@@ -0,0 +1,351 @@
+/*
+ * 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");
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java
new file mode 100644 (file)
index 0000000..cdab926
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java
new file mode 100644 (file)
index 0000000..216d214
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerHelp.java
new file mode 100644 (file)
index 0000000..8dbe85b
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * 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;
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerOptions.java
new file mode 100644 (file)
index 0000000..cf193c2
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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());
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java
new file mode 100644 (file)
index 0000000..1f539ba
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java b/monkeyrunner/src/com/android/monkeyrunner/PhysicalButton.java
new file mode 100644 (file)
index 0000000..f0525a0
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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;
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java b/monkeyrunner/src/com/android/monkeyrunner/ScriptRunner.java
new file mode 100644 (file)
index 0000000..c247a5f
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java
new file mode 100644 (file)
index 0000000..63badf5
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java
new file mode 100644 (file)
index 0000000..dedc1ea
--- /dev/null
@@ -0,0 +1,530 @@
+/*
+ * 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);
+                }
+            }
+        });
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java
new file mode 100644 (file)
index 0000000..fc32600
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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;
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java b/monkeyrunner/src/com/android/monkeyrunner/adb/CommandOutputCapture.java
new file mode 100644 (file)
index 0000000..9f99a7a
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java b/monkeyrunner/src/com/android/monkeyrunner/adb/LinearInterpolator.java
new file mode 100644 (file)
index 0000000..e39fefd
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java b/monkeyrunner/src/com/android/monkeyrunner/adb/LoggingOutputReceiver.java
new file mode 100644 (file)
index 0000000..b78aff3
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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;
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java
new file mode 100644 (file)
index 0000000..7e31ea5
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ImageUtils.java
new file mode 100644 (file)
index 0000000..c3eaf01
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/SixteenBitColorModel.java
new file mode 100644 (file)
index 0000000..06ab939
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/ThirtyTwoBitColorModel.java
new file mode 100644 (file)
index 0000000..d4e47ea
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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();
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java
new file mode 100644 (file)
index 0000000..e199a75
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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();
+                    }
+                });
+            }
+        });
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java
new file mode 100644 (file)
index 0000000..7f5a7d8
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ * 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();
+    }
+
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java b/monkeyrunner/src/com/android/monkeyrunner/controller/VariableFrame.java
new file mode 100644 (file)
index 0000000..9015b5d
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * 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;
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java b/monkeyrunner/src/com/android/monkeyrunner/doc/MonkeyRunnerExported.java
new file mode 100644 (file)
index 0000000..dd3eb0b
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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
diff --git a/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java b/monkeyrunner/src/com/android/monkeyrunner/exceptions/MonkeyRunnerException.java
new file mode 100644 (file)
index 0000000..8fe4143
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java b/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java
new file mode 100644 (file)
index 0000000..c2fa5f7
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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.
+    }
+}
diff --git a/monkeyrunner/src/resources/com/android/monkeyrunner/html.cs b/monkeyrunner/src/resources/com/android/monkeyrunner/html.cs
new file mode 100644 (file)
index 0000000..7d2c93f
--- /dev/null
@@ -0,0 +1,25 @@
+<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>
diff --git a/monkeyrunner/src/resources/com/android/monkeyrunner/text.cs b/monkeyrunner/src/resources/com/android/monkeyrunner/text.cs
new file mode 100644 (file)
index 0000000..4a4af5f
--- /dev/null
@@ -0,0 +1,9 @@
+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 ?>
diff --git a/monkeyrunner/test/Android.mk b/monkeyrunner/test/Android.mk
new file mode 100644 (file)
index 0000000..6e2233b
--- /dev/null
@@ -0,0 +1,23 @@
+#
+# 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)
diff --git a/monkeyrunner/test/com/android/monkeyrunner/AllTests.java b/monkeyrunner/test/com/android/monkeyrunner/AllTests.java
new file mode 100644 (file)
index 0000000..9616759
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java b/monkeyrunner/test/com/android/monkeyrunner/ImageUtilsTest.java
new file mode 100644 (file)
index 0000000..f07c2f3
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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));
+    }
+}
diff --git a/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java b/monkeyrunner/test/com/android/monkeyrunner/JythonUtilsTest.java
new file mode 100644 (file)
index 0000000..5b8c8f9
--- /dev/null
@@ -0,0 +1,224 @@
+/*
+ * 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);
+    }
+}
diff --git a/monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java b/monkeyrunner/test/com/android/monkeyrunner/MonkeyRunnerOptionsTest.java
new file mode 100644 (file)
index 0000000..fd23721
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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());
+  }
+}
diff --git a/monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java b/monkeyrunner/test/com/android/monkeyrunner/adb/AdbMonkeyDeviceTest.java
new file mode 100644 (file)
index 0000000..258e184
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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"));
+    }
+}
diff --git a/monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java b/monkeyrunner/test/com/android/monkeyrunner/adb/LinearInterpolatorTest.java
new file mode 100644 (file)
index 0000000..00670ce
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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));
+        }
+    }
+}
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/instrument_result.txt
new file mode 100644 (file)
index 0000000..c127c0f
--- /dev/null
@@ -0,0 +1,10 @@
+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
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt b/monkeyrunner/test/resources/com/android/monkeyrunner/adb/multiline_instrument_result.txt
new file mode 100644 (file)
index 0000000..32fd901
--- /dev/null
@@ -0,0 +1,15 @@
+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
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png
new file mode 100644 (file)
index 0000000..9ef1800
Binary files /dev/null and b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.png differ
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw
new file mode 100644 (file)
index 0000000..99ec013
Binary files /dev/null and b/monkeyrunner/test/resources/com/android/monkeyrunner/image1.raw differ
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png
new file mode 100644 (file)
index 0000000..03ff0c1
Binary files /dev/null and b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.png differ
diff --git a/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw
new file mode 100644 (file)
index 0000000..06e5b47
Binary files /dev/null and b/monkeyrunner/test/resources/com/android/monkeyrunner/image2.raw differ