diff options
author | Terence Haddock <thaddock@google.com> | 2011-03-30 13:02:28 +0200 |
---|---|---|
committer | Terence Haddock <thaddock@google.com> | 2011-04-11 09:41:07 +0200 |
commit | 4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3 (patch) | |
tree | 50094087e9b2bfd8ea5bb361d28c7079536d1246 /monkeyrunner/src | |
parent | 9e77016f566f52fa89fd732bea8e962e28a62981 (diff) | |
download | sdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.zip sdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.tar.gz sdk-4fe2f2f34d3e16f1ab9e64aacdff1a5cf99065c3.tar.bz2 |
Initial version of "easy monkeyrunner".
Change-Id: I4fce0fb00eac1ed59d0b8a3bd4ac8d168b36b4e0
Diffstat (limited to 'monkeyrunner/src')
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. |