diff --git a/docs/finding-elements.md b/docs/finding-elements.md
index 06303a4bd29..b17e184bf56 100644
--- a/docs/finding-elements.md
+++ b/docs/finding-elements.md
@@ -11,6 +11,7 @@ Appium supports a subset of the WebDriver locator strategies:
Appium additionally supports some of the [Mobile JSON Wire Protocol](https://code.google.com/p/selenium/source/browse/spec-draft.md?repo=mobile) locator strategies
* `-ios uiautomation`: a string corresponding to a recursive element search using the UIAutomation library (iOS-only)
+* `-android uiautomator`: a string corresponding to a recursive element search using the UiAutomator Api (Android-only)
* `accessibility id`: a string corresponding to a recursive element search using the Id/Name that the native Accessibility options utilize.
###Tag name mapping
@@ -131,6 +132,14 @@ WD.js:
driver.element('-ios uiautomation', '.elements()[1].cells()[2]').getAttribute('name');
```
+### Using the "-android uiautomator" locator strategy
+
+WD.js:
+
+```js
+driver.element('-android uiautomator', 'new UiSelector().clickable(true)').getAttribute('name');
+```
+
# FindAndAct
If you want, you can find and act on an element in a single command (iOS-only).
diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/exceptions/UiSelectorSyntaxException.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/exceptions/UiSelectorSyntaxException.java
new file mode 100644
index 00000000000..af661679f9d
--- /dev/null
+++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/exceptions/UiSelectorSyntaxException.java
@@ -0,0 +1,17 @@
+package io.appium.android.bootstrap.exceptions;
+
+import io.appium.android.bootstrap.utils.UiSelectorParser;
+
+/**
+ * An exception involving an {@link UiSelectorParser}.
+ *
+ * @param msg
+ * A descriptive message describing the error.
+ */
+@SuppressWarnings("serial")
+public class UiSelectorSyntaxException extends Exception {
+
+ public UiSelectorSyntaxException(final String msg) {
+ super(msg);
+ }
+}
diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
index 7ed175f4b39..5e39cb4e460 100644
--- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
+++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/Find.java
@@ -1,37 +1,23 @@
package io.appium.android.bootstrap.handler;
-import io.appium.android.bootstrap.AndroidCommand;
-import io.appium.android.bootstrap.AndroidCommandResult;
-import io.appium.android.bootstrap.AndroidElement;
-import io.appium.android.bootstrap.AndroidElementClassMap;
-import io.appium.android.bootstrap.AndroidElementsHash;
-import io.appium.android.bootstrap.CommandHandler;
-import io.appium.android.bootstrap.Dynamic;
-import io.appium.android.bootstrap.Logger;
-import io.appium.android.bootstrap.WDStatus;
-import io.appium.android.bootstrap.exceptions.AndroidCommandException;
-import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
-import io.appium.android.bootstrap.exceptions.ElementNotInHashException;
-import io.appium.android.bootstrap.exceptions.InvalidStrategyException;
-import io.appium.android.bootstrap.exceptions.UnallowedTagNameException;
+import android.os.Build;
+import com.android.uiautomator.core.UiObject;
+import com.android.uiautomator.core.UiObjectNotFoundException;
+import com.android.uiautomator.core.UiScrollable;
+import com.android.uiautomator.core.UiSelector;
+import io.appium.android.bootstrap.*;
+import io.appium.android.bootstrap.exceptions.*;
import io.appium.android.bootstrap.selector.Strategy;
+import io.appium.android.bootstrap.utils.UiSelectorParser;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import android.os.Build;
-
-import com.android.uiautomator.core.UiObject;
-import com.android.uiautomator.core.UiObjectNotFoundException;
-import com.android.uiautomator.core.UiScrollable;
-import com.android.uiautomator.core.UiSelector;
-
/**
* This handler is used to find elements in the Android UI.
*
@@ -44,6 +30,7 @@ public class Find extends CommandHandler {
AndroidElementsHash elements = AndroidElementsHash.getInstance();
Dynamic dynamic = new Dynamic();
public static JSONObject apkStrings = null;
+ UiSelectorParser uiSelectorParser = new UiSelectorParser();
private Object[] cascadeChildSels(final ArrayList tail,
final ArrayList tailOuts) {
@@ -277,6 +264,8 @@ public AndroidCommandResult execute(final AndroidCommand command)
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
} catch (final AndroidCommandException e) {
return new AndroidCommandResult(WDStatus.UNKNOWN_ERROR, e.getMessage());
+ } catch (final UiSelectorSyntaxException e) {
+ return new AndroidCommandResult(WDStatus.UNKNOWN_COMMAND, e.getMessage());
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
@@ -412,7 +401,7 @@ private AndroidCommandResult findElementsByIndexPaths(final String selector,
private List getSelector(final Strategy strategy,
final String text, final boolean many) throws InvalidStrategyException,
AndroidCommandException, UnallowedTagNameException,
- ElementNotFoundException {
+ ElementNotFoundException, UiSelectorSyntaxException {
final List selectors = new ArrayList();
UiSelector sel = new UiSelector();
@@ -477,6 +466,17 @@ private List getSelector(final Strategy strategy,
break;
case XPATH:
break;
+ case ANDROID_UIAUTOMATOR:
+ try {
+ sel = uiSelectorParser.parse(text);
+ } catch (UiSelectorSyntaxException e) {
+ throw new UiSelectorSyntaxException("Could not parse UiSelector argument: " + e.getMessage());
+ }
+ if (!many) {
+ sel = sel.instance(0);
+ }
+ selectors.add(sel);
+ break;
case LINK_TEXT:
case PARTIAL_LINK_TEXT:
case CSS_SELECTOR:
diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/selector/Strategy.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/selector/Strategy.java
index c08501af4db..22845ac9108 100644
--- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/selector/Strategy.java
+++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/selector/Strategy.java
@@ -10,7 +10,7 @@ public enum Strategy {
CLASS_NAME(0, "class name"), CSS_SELECTOR(1, "css selector"), ID(2, "id"), NAME(
3, "name"), LINK_TEXT(4, "link text"), PARTIAL_LINK_TEXT(5,
"partial link text"), TAG_NAME(6, "tag name"), XPATH(7, "xpath"), DYNAMIC(
- 8, "dynamic"), ACCESSIBILITY_ID(9, "accessibility id");
+ 8, "dynamic"), ACCESSIBILITY_ID(9, "accessibility id"), ANDROID_UIAUTOMATOR(10, "-android uiautomator");
public static Strategy fromString(final String text) throws InvalidStrategyException {
if (text != null) {
diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java
new file mode 100644
index 00000000000..4f652923f4a
--- /dev/null
+++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/utils/UiSelectorParser.java
@@ -0,0 +1,190 @@
+package io.appium.android.bootstrap.utils;
+
+import com.android.uiautomator.core.UiSelector;
+import io.appium.android.bootstrap.exceptions.UiSelectorSyntaxException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+
+/**
+ * For parsing strings passed in for the "-android uiautomator" locator strategy
+ */
+public class UiSelectorParser {
+
+ private String text;
+ private UiSelector selector;
+ private final static Method[] methods = UiSelector.class.getDeclaredMethods();
+
+ public UiSelector parse(String textToParse) throws UiSelectorSyntaxException {
+ selector = new UiSelector();
+ text = cleanseText(textToParse);
+
+ while (text.length() > 0) {
+ consumePeriod();
+ consumeFunctionCall();
+ }
+
+ return selector;
+ }
+
+ // prepares text for the main parsing loop
+ private String cleanseText(String dirtyText) {
+ String cleanText = dirtyText.trim();
+
+ if (cleanText.startsWith("new UiSelector()")) {
+ cleanText = cleanText.substring(16);
+ }
+ else if (cleanText.startsWith("UiSelector()")) {
+ cleanText = cleanText.substring(12);
+ }
+ else if (!cleanText.startsWith(".")){
+ cleanText = "." + cleanText;
+ }
+
+ return cleanText;
+ }
+
+ private void consumePeriod() throws UiSelectorSyntaxException {
+ if (text.startsWith(".")) {
+ text = text.substring(1);
+ }
+ else {
+ throw new UiSelectorSyntaxException("Expected \".\" but saw \"" + text.charAt(0) + "\"");
+ }
+ }
+
+ /*
+ * consume [a-z]* then an open paren, this is our methodName
+ * consume .* and count open/close parens until the original open paren is close, this is our argument
+ *
+ */
+ private void consumeFunctionCall() throws UiSelectorSyntaxException {
+ String methodName;
+ StringBuilder argument = new StringBuilder();
+
+ int parenIndex = text.indexOf('(');
+ methodName = text.substring(0, parenIndex);
+
+ int index = parenIndex+1;
+ int parenCount = 1;
+ while (parenCount > 0) {
+ try {
+ switch (text.charAt(index)) {
+ case ')':
+ parenCount--;
+ if (parenCount > 0) {
+ argument.append(text.charAt(index));
+ }
+ break;
+ case '(':
+ parenCount++;
+ argument.append(text.charAt(index));
+ break;
+ default:
+ argument.append(text.charAt(index));
+ }
+ } catch (StringIndexOutOfBoundsException e) {
+ throw new UiSelectorSyntaxException("unclosed paren in expression");
+ }
+ index++;
+ }
+ if (argument.length() < 1) {
+ throw new UiSelectorSyntaxException(methodName + " method expects an argument");
+ }
+
+ //add two for parentheses surrounding arg
+ text = text.substring(methodName.length() + argument.length() + 2);
+
+ ArrayList overloadedMethods = getSelectorMethods(methodName);
+ if (overloadedMethods.size() < 1) {
+ throw new UiSelectorSyntaxException("UiSelector has no " + methodName + " method");
+ }
+
+ selector = applyArgToMethods(overloadedMethods, argument.toString());
+ }
+
+ private ArrayList getSelectorMethods(String methodName) {
+ ArrayList ret = new ArrayList();
+ for (Method method : methods) {
+ if (method.getName().equals(methodName)) {
+ ret.add(method);
+ }
+ }
+ return ret;
+ }
+
+ private UiSelector applyArgToMethods(ArrayList methods, String argument) throws UiSelectorSyntaxException {
+
+ Object arg = null;
+ Method ourMethod = null;
+ UiSelectorSyntaxException exThrown = null;
+ for (Method method : methods) {
+ try {
+ Type parameterType = method.getGenericParameterTypes()[0];
+ arg = coerceArgToType(parameterType, argument);
+ ourMethod = method;
+ } catch (UiSelectorSyntaxException e) {
+ exThrown = e;
+ }
+ }
+
+ if (ourMethod == null || arg == null) {
+ if (exThrown != null) {
+ throw exThrown;
+ } else {
+ throw new UiSelectorSyntaxException("Could not apply argument " + argument + " to UiSelector method");
+ }
+ }
+
+ try {
+ return (UiSelector)ourMethod.invoke(selector, arg);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ throw new UiSelectorSyntaxException("problem using reflection to call this method");
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ throw new UiSelectorSyntaxException("problem using reflection to call this method");
+ }
+
+ }
+
+ private Object coerceArgToType(Type type, String argument) throws UiSelectorSyntaxException {
+ if (type == boolean.class) {
+ if (argument.equals("true")) {
+ return true;
+ }
+ if (argument.equals("false")) {
+ return false;
+ }
+ throw new UiSelectorSyntaxException(argument + " is not a boolean");
+ }
+
+ if (type == String.class) {
+ if (argument.charAt(0) != '"' || argument.charAt(argument.length()-1) != '"') {
+ throw new UiSelectorSyntaxException(argument + " is not a string");
+ }
+ return argument.substring(1, argument.length()-1);
+ }
+
+ if (type == int.class) {
+ return Integer.parseInt(argument);
+ }
+
+ if (type.toString().equals("java.lang.Class")) {
+ try {
+ return Class.forName(argument);
+ } catch (ClassNotFoundException e) {
+ throw new UiSelectorSyntaxException(argument + " class could not be found");
+ }
+ }
+
+ if (type == UiSelector.class) {
+ UiSelectorParser parser = new UiSelectorParser();
+ return parser.parse(argument);
+ }
+
+ throw new UiSelectorSyntaxException("Could not coerce " + argument + " to any sort of Type");
+ }
+}
diff --git a/lib/devices/common.js b/lib/devices/common.js
index 12f3e5a6050..c2c96ce76d9 100644
--- a/lib/devices/common.js
+++ b/lib/devices/common.js
@@ -211,7 +211,8 @@ exports.checkValidLocStrat = function (strat, includeWeb, cb) {
var nativeStrats = [
'-ios uiautomation',
'accessibility id',
- '-real xpath'
+ '-real xpath',
+ '-android uiautomator'
];
var webStrats = [
'link text',
diff --git a/test/functional/android/apidemos/find-element-specs.js b/test/functional/android/apidemos/find-element-specs.js
index 8add437311a..50ff5a8fdac 100644
--- a/test/functional/android/apidemos/find-element-specs.js
+++ b/test/functional/android/apidemos/find-element-specs.js
@@ -224,6 +224,74 @@ describe("apidemo - find elements -", function () {
});
});
+ describe('find elements by -android uiautomator locator strategy', function () {
+ it('should find elements with a boolean argument', function (done) {
+ driver.elements('-android uiautomator', 'new UiSelector().clickable(true)').then(function (els) {
+ els.length.should.equal(13);
+ }).nodeify(done);
+ });
+ it('should find elements without prepending "new UiSelector()"', function (done) {
+ driver.elements('-android uiautomator', '.clickable(true)').then(function (els) {
+ els.length.should.equal(13);
+ }).nodeify(done);
+ });
+ it('should find elements without prepending "new UiSelector()."', function (done) {
+ driver.elements('-android uiautomator', 'clickable(true)').then(function (els) {
+ els.length.should.equal(13);
+ }).nodeify(done);
+ });
+ it('should find elements without prepending "new "', function (done) {
+ driver.elements('-android uiautomator', 'UiSelector().clickable(true)').then(function (els) {
+ els.length.should.equal(13);
+ }).nodeify(done);
+ });
+ it('should find an element with an int argument', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().index(0)').getTagName().then(function (tag) {
+ tag.should.equal('android.widget.FrameLayout');
+ }).nodeify(done);
+ });
+ it('should find an element with a string argument', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().description("Animation")').then(function (el) {
+ el.should.exist;
+ }).nodeify(done);
+ });
+ it('should find an element with an overloaded method argument', function (done) {
+ driver.elements('-android uiautomator', 'new UiSelector().className("android.widget.TextView")').then(function (els) {
+ els.length.should.equal(12);
+ }).nodeify(done);
+ });
+ it('should find an element with a Class method argument', function (done) {
+ driver.elements('-android uiautomator', 'new UiSelector().className(android.widget.TextView)').then(function (els) {
+ els.length.should.equal(12);
+ }).nodeify(done);
+ });
+ it('should find an element with a long chain of methods', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().clickable(true).className(android.widget.TextView).index(0)').text().then(function (text) {
+ text.should.equal('Accessibility');
+ }).nodeify(done);
+ });
+ it('should find an element with recursive UiSelectors', function (done) {
+ driver.elements('-android uiautomator', 'new UiSelector().childSelector(new UiSelector().clickable(true)).clickable(true)').then(function (els) {
+ els.length.should.equal(2);
+ }).nodeify(done);
+ });
+ it('should not find an element with bad syntax', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().clickable((true)')
+ .should.be.rejectedWith(/status: 9/)
+ .nodeify(done);
+ });
+ it('should not find an element with a made up method', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().drinkable(true)')
+ .should.be.rejectedWith(/status: 9/)
+ .nodeify(done);
+ });
+ it('should not find an element which does not exist', function (done) {
+ driver.element('-android uiautomator', 'new UiSelector().description("chuckwudi")')
+ .should.be.rejectedWith(/status: 7/)
+ .nodeify(done);
+ });
+ });
+
describe('unallowed tag names', function () {
it('should not find secure fields', function (done) {
driver