aboutsummaryrefslogtreecommitdiffstats
path: root/monkeyrunner/src
diff options
context:
space:
mode:
authorTerence Haddock <thaddock@google.com>2011-03-30 13:02:28 +0200
committerTerence Haddock <thaddock@google.com>2011-04-11 09:41:07 +0200
commit4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3 (patch)
tree50094087e9b2bfd8ea5bb361d28c7079536d1246 /monkeyrunner/src
parent9e77016f566f52fa89fd732bea8e962e28a62981 (diff)
downloadsdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.zip
sdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.tar.gz
sdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.tar.bz2
Initial version of "easy monkeyrunner".
Change-Id: I4fce0fb00eac1ed59d0b8a3bd4ac8d168b36b4e0
Diffstat (limited to 'monkeyrunner/src')
-rw-r--r--monkeyrunner/src/Android.mk4
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java18
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java14
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java6
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/easy/By.java80
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java215
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java124
-rw-r--r--monkeyrunner/src/com/android/monkeyrunner/easy/README27
8 files changed, 487 insertions, 1 deletions
diff --git a/monkeyrunner/src/Android.mk b/monkeyrunner/src/Android.mk
index 38be272..8cca0bc 100644
--- a/monkeyrunner/src/Android.mk
+++ b/monkeyrunner/src/Android.mk
@@ -24,7 +24,9 @@ LOCAL_JAVA_LIBRARIES := \
jython \
guavalib \
jsilver \
- sdklib
+ sdklib \
+ hierarchyviewerlib \
+ swt
LOCAL_JAVA_RESOURCE_DIRS := resources
diff --git a/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java b/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java
index 7054695..badddff 100644
--- a/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java
+++ b/monkeyrunner/src/com/android/monkeyrunner/JythonUtils.java
@@ -23,9 +23,11 @@ import java.text.BreakIterator;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -483,4 +485,20 @@ public final class JythonUtils {
lines.add(currentLine.toString());
return lines;
}
+
+ /**
+ * Obtain the set of method names available from Python.
+ *
+ * @param clazz Class to inspect.
+ * @return set of method names annotated with {@code MonkeyRunnerExported}.
+ */
+ public static Set<String> getMethodNames(Class<?> clazz) {
+ HashSet<String> methodNames = new HashSet<String>();
+ for (Method m: clazz.getMethods()) {
+ if (m.isAnnotationPresent(MonkeyRunnerExported.class)) {
+ methodNames.add(m.getName());
+ }
+ }
+ return methodNames;
+ }
}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java
index 0f4362a..37b1cda 100644
--- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java
+++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java
@@ -31,6 +31,8 @@ import org.python.core.PyObject;
import org.python.core.PyTuple;
import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+import com.android.monkeyrunner.easy.HierarchyViewer;
+
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.collect.Collections2;
@@ -82,6 +84,18 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit {
*/
public abstract void dispose();
+ /**
+ * @return hierarchy viewer implementation for querying state of the view
+ * hierarchy.
+ */
+ public abstract HierarchyViewer getHierarchyViewer();
+
+ @MonkeyRunnerExported(doc = "Get the HierarchyViewer object for the device.",
+ returns = "A HierarchyViewer object")
+ public HierarchyViewer getHierarchyViewer(PyObject[] args, String[] kws) {
+ return getHierarchyViewer();
+ }
+
@MonkeyRunnerExported(doc =
"Gets the device's screen buffer, yielding a screen capture of the entire display.",
returns = "A MonkeyImage object (a bitmap wrapper)")
diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java
index 41c4d02..614e656 100644
--- a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java
+++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java
@@ -29,6 +29,7 @@ import com.android.monkeyrunner.MonkeyDevice;
import com.android.monkeyrunner.MonkeyImage;
import com.android.monkeyrunner.MonkeyManager;
import com.android.monkeyrunner.adb.LinearInterpolator.Point;
+import com.android.monkeyrunner.easy.HierarchyViewer;
import java.io.IOException;
import java.net.InetAddress;
@@ -81,6 +82,11 @@ public class AdbMonkeyDevice extends MonkeyDevice {
manager = null;
}
+ @Override
+ public HierarchyViewer getHierarchyViewer() {
+ return new HierarchyViewer(device);
+ }
+
private void executeAsyncCommand(final String command,
final LoggingOutputReceiver logger) {
executor.submit(new Runnable() {
diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/By.java b/monkeyrunner/src/com/android/monkeyrunner/easy/By.java
new file mode 100644
index 0000000..a8be6c0
--- /dev/null
+++ b/monkeyrunner/src/com/android/monkeyrunner/easy/By.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 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.easy;
+
+import com.google.common.base.Preconditions;
+
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.monkeyrunner.JythonUtils;
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.python.core.ArgParser;
+import org.python.core.ClassDictInit;
+import org.python.core.PyObject;
+
+/**
+ * Select a view object based on some criteria.
+ *
+ * Currently supports the By.id criteria to search for an element by id.
+ * In the future it will support other criteria such as:
+ * By.classid - search by class.
+ * By.hash - search by hashcode
+ * and recursively searching under an already selected object.
+ *
+ * WARNING: This API is under development, expect the interface to change
+ * without notice.
+ *
+ * TODO: Implement other selectors, like classid, hash, and so on.
+ */
+public class By extends PyObject implements ClassDictInit {
+ public static void classDictInit(PyObject dict) {
+ JythonUtils.convertDocAnnotationsForClass(By.class, dict);
+ }
+
+ private String id;
+
+ By(String id) {
+ this.id = id;
+ }
+
+ @MonkeyRunnerExported(doc = "Select an object by id.",
+ args = { "id" },
+ argDocs = { "The identifier of the object." })
+ public static By id(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ String id = ap.getString(0);
+ return new By(id);
+ }
+
+ /**
+ * Find the selected view from the root view node.
+ */
+ ViewNode find(ViewNode rootNode) {
+ if (rootNode.id.equals(id)) {
+ return rootNode;
+ }
+ for (ViewNode child : rootNode.children) {
+ ViewNode found = find(child);
+ if (found != null) {
+ return found;
+ }
+ }
+ return null;
+ }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java
new file mode 100644
index 0000000..8e6ec0f
--- /dev/null
+++ b/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2011 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.easy;
+
+import com.google.common.base.Preconditions;
+
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.device.ViewNode.Property;
+import com.android.monkeyrunner.JythonUtils;
+import com.android.monkeyrunner.MonkeyDevice;
+import com.android.monkeyrunner.MonkeyDevice.TouchPressType;
+import com.android.monkeyrunner.doc.MonkeyRunnerExported;
+
+import org.eclipse.swt.graphics.Point;
+import org.python.core.ArgParser;
+import org.python.core.ClassDictInit;
+import org.python.core.Py;
+import org.python.core.PyException;
+import org.python.core.PyInteger;
+import org.python.core.PyObject;
+import org.python.core.PyTuple;
+
+import java.util.Set;
+
+/**
+ * Extends {@link MonkeyDevice} to support looking up views using a 'selector'.
+ * Currently, only identifiers can be used as a selector. All methods on
+ * MonkeyDevice can be used on this class in Python.
+ *
+ * WARNING: This API is under development, expect the interface to change
+ * without notice.
+ */
+@MonkeyRunnerExported(doc = "MonkeyDevice with easier methods to refer to objects.")
+public class EasyMonkeyDevice extends PyObject implements ClassDictInit {
+ public static void classDictInit(PyObject dict) {
+ JythonUtils.convertDocAnnotationsForClass(EasyMonkeyDevice.class, dict);
+ }
+
+ private MonkeyDevice mDevice;
+ private HierarchyViewer mHierarchyViewer;
+
+ private static final Set<String> EXPORTED_METHODS =
+ JythonUtils.getMethodNames(EasyMonkeyDevice.class);
+
+ @MonkeyRunnerExported(doc = "Creates EasyMonkeyDevice with an underlying MonkeyDevice.",
+ args = { "device" },
+ argDocs = { "MonkeyDevice to extend." })
+ public EasyMonkeyDevice(MonkeyDevice device) {
+ this.mDevice = device;
+ this.mHierarchyViewer = device.getHierarchyViewer();
+ }
+
+ @MonkeyRunnerExported(doc = "Sends a touch event to the selected object.",
+ args = { "selector", "type" },
+ argDocs = {
+ "The selector identifying the object.",
+ "The event type as returned by TouchPressType()." })
+ public void touch(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+ ViewNode node = mHierarchyViewer.findView(selector);
+ if (node == null) {
+ throw new PyException(Py.ValueError,
+ String.format("View not found: %s", selector));
+ }
+ Point p = HierarchyViewer.getAbsoluteCenterOfView(node);
+
+ PyObject[] otherArgs = new PyObject[3];
+ otherArgs[0] = new PyInteger(p.x);
+ otherArgs[1] = new PyInteger(p.y);
+ otherArgs[2] = args[1];
+
+ mDevice.touch(otherArgs, kws);
+ }
+
+ @MonkeyRunnerExported(doc = "Types a string into the specified object.",
+ args = { "selector", "text" },
+ argDocs = {
+ "The selector identifying the object.",
+ "The text to type into the object." })
+ public void type(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+ String text = ap.getString(1);
+
+ ViewNode node = mHierarchyViewer.findView(selector);
+ if (node == null) {
+ throw new PyException(Py.ValueError,
+ String.format("View not found: %s", selector));
+ }
+
+ Point p = HierarchyViewer.getAbsoluteCenterOfView(node);
+ mDevice.touch(p.x, p.y, TouchPressType.DOWN_AND_UP);
+ mDevice.type(text);
+ }
+
+ @MonkeyRunnerExported(doc = "Locates the coordinates of the selected object.",
+ args = { "selector" },
+ argDocs = { "The selector identifying the object." },
+ returns = "Tuple containing (x,y,w,h) location and size.")
+ public PyTuple locate(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+
+ ViewNode node = mHierarchyViewer.findView(selector);
+ Point p = HierarchyViewer.getAbsolutePositionOfView(node);
+ PyTuple tuple = new PyTuple(
+ new PyInteger(p.x),
+ new PyInteger(p.y),
+ new PyInteger(node.width),
+ new PyInteger(node.height));
+ return tuple;
+ }
+
+ @MonkeyRunnerExported(doc = "Checks if the specified object exists.",
+ args = { "selector" },
+ returns = "True if the object exists.",
+ argDocs = { "The selector identifying the object." })
+ public boolean exists(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+
+ ViewNode node = mHierarchyViewer.findView(selector);
+ return node != null;
+ }
+
+ @MonkeyRunnerExported(doc = "Checks if the specified object is visible.",
+ args = { "selector" },
+ returns = "True if the object is visible.",
+ argDocs = { "The selector identifying the object." })
+ public boolean visible(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+
+ ViewNode node = mHierarchyViewer.findView(selector);
+ boolean ret = (node != null)
+ && node.namedProperties.containsKey("getVisibility()")
+ && "VISIBLE".equalsIgnoreCase(
+ node.namedProperties.get("getVisibility()").value);
+ return ret;
+ }
+
+ @MonkeyRunnerExported(doc = "Obtain the text in the selected input box.",
+ args = { "selector" },
+ argDocs = { "The selector identifying the object." },
+ returns = "Text in the selected input box.")
+ public String getText(PyObject[] args, String[] kws) {
+ ArgParser ap = JythonUtils.createArgParser(args, kws);
+ Preconditions.checkNotNull(ap);
+
+ By selector = getSelector(ap, 0);
+
+ ViewNode node = mHierarchyViewer.findView(selector);
+ if (node == null) {
+ throw new RuntimeException("Node not found");
+ }
+ Property textProperty = node.namedProperties.get("text:mText");
+ if (textProperty == null) {
+ throw new RuntimeException("No text property on node");
+ }
+ return textProperty.value;
+ }
+
+ @MonkeyRunnerExported(doc = "Gets the id of the focused window.",
+ returns = "The symbolic id of the focused window or None.")
+ public String getFocusedWindowId(PyObject[] args, String[] kws) {
+ return mHierarchyViewer.getFocusedWindowName();
+ }
+
+ /**
+ * Forwards unknown methods to the original MonkeyDevice object.
+ */
+ @Override
+ public PyObject __findattr_ex__(String name) {
+ if (!EXPORTED_METHODS.contains(name)) {
+ return mDevice.__findattr_ex__(name);
+ }
+ return super.__findattr_ex__(name);
+ }
+
+ /**
+ * Get the selector object from the argument parser.
+ *
+ * @param ap argument parser to get it from.
+ * @param i argument index.
+ * @return selector object.
+ */
+ private By getSelector(ArgParser ap, int i) {
+ return (By)ap.getPyObject(0).__tojava__(By.class);
+ }
+} \ No newline at end of file
diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java b/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java
new file mode 100644
index 0000000..5d6911e
--- /dev/null
+++ b/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 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.easy;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log;
+import com.android.hierarchyviewerlib.device.DeviceBridge;
+import com.android.hierarchyviewerlib.device.ViewNode;
+import com.android.hierarchyviewerlib.device.Window;
+
+import org.eclipse.swt.graphics.Point;
+
+/**
+ * Class for querying the view hierarchy of the device.
+ */
+public class HierarchyViewer {
+ public static final String TAG = "hierarchyviewer";
+
+ private IDevice mDevice;
+
+ /**
+ * Constructs the hierarchy viewer for the specified device.
+ *
+ * @param device The Android device to connect to.
+ */
+ public HierarchyViewer(IDevice device) {
+ this.mDevice = device;
+ setupViewServer();
+ }
+
+ private void setupViewServer() {
+ DeviceBridge.setupDeviceForward(mDevice);
+ if (!DeviceBridge.isViewServerRunning(mDevice)) {
+ if (!DeviceBridge.startViewServer(mDevice)) {
+ // TODO: Get rid of this delay.
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ }
+ if (!DeviceBridge.startViewServer(mDevice)) {
+ Log.e(TAG, "Unable to debug device " + mDevice);
+ throw new RuntimeException("Could not connect to the view server");
+ }
+ return;
+ }
+ }
+ DeviceBridge.loadViewServerInfo(mDevice);
+ }
+
+ /**
+ * Finds a view using a selector. Currently only supports selectors which
+ * specify an id.
+ *
+ * @param selector selector for the view.
+ * @return view with the specified ID, or {@code null} if no view found.
+ */
+ public ViewNode findView(By selector) {
+ ViewNode rootNode = DeviceBridge.loadWindowData(
+ new Window(mDevice, "", 0xffffffff));
+ if (rootNode == null) {
+ throw new RuntimeException("Could not dump view");
+ }
+ return selector.find(rootNode);
+ }
+
+ /**
+ * Gets the window that currently receives the focus.
+ *
+ * @return name of the window that currently receives the focus.
+ */
+ public String getFocusedWindowName() {
+ int id = DeviceBridge.getFocusedWindow(mDevice);
+ Window[] windows = DeviceBridge.loadWindows(mDevice);
+ for (Window w : windows) {
+ if (w.getHashCode() == id)
+ return w.getTitle();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the absolute x/y position of the view node.
+ *
+ * @param node view node to find position of.
+ * @return point specifying the x/y position of the node.
+ */
+ public static Point getAbsolutePositionOfView(ViewNode node) {
+ int x = node.left;
+ int y = node.top;
+ ViewNode p = node.parent;
+ while (p != null) {
+ x += p.left - p.scrollX;
+ y += p.top - p.scrollY;
+ p = p.parent;
+ }
+ return new Point(x, y);
+ }
+
+ /**
+ * Gets the absolute x/y center of the specified view node.
+ *
+ * @param node view node to find position of.
+ * @return absolute x/y center of the specified view node.
+ */
+ public static Point getAbsoluteCenterOfView(ViewNode node) {
+ Point point = getAbsolutePositionOfView(node);
+ return new Point(
+ point.x + (node.width / 2), point.y + (node.height / 2));
+ }
+}
diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/README b/monkeyrunner/src/com/android/monkeyrunner/easy/README
new file mode 100644
index 0000000..239bedd
--- /dev/null
+++ b/monkeyrunner/src/com/android/monkeyrunner/easy/README
@@ -0,0 +1,27 @@
+com.android.monkeyrunner.easy contains classes intended to make it easier
+to interact with applications using the MonkeyRunner framework. Instead of
+referencing a button or input box by x,y coordinate, they can be referenced
+by identifier, as in the following Python example:
+
+##############################################################################
+
+from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
+from com.android.monkeyrunner.easy import EasyMonkeyDevice
+from com.android.monkeyrunner.easy import By
+
+# Connect to the current device.
+device = MonkeyRunner.waitForConnection()
+
+# Use the EasyMonkey API, all methods on device are available in easy_device.
+easy_device = EasyMonkeyDevice(device)
+
+if not easy_device.visible(By.id('id/all_apps_button')):
+ raise Error('Could not find the "all apps" button')
+
+print "Location of element:", easy_device.locate(By.id('id/all_apps_button'))
+
+easy_device.touch(By.id('id/all_apps_button'), 'DOWN_AND_UP')
+
+##############################################################################
+
+WARNING: This API is under development and may change without notice.