aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/finders
diff options
context:
space:
mode:
Diffstat (limited to 'src/io/appium/droiddriver/finders')
-rw-r--r--src/io/appium/droiddriver/finders/Attribute.java53
-rw-r--r--src/io/appium/droiddriver/finders/By.java252
-rw-r--r--src/io/appium/droiddriver/finders/ByXPath.java226
-rw-r--r--src/io/appium/droiddriver/finders/ChainFinder.java51
-rw-r--r--src/io/appium/droiddriver/finders/Finder.java46
-rw-r--r--src/io/appium/droiddriver/finders/MatchFinder.java72
-rw-r--r--src/io/appium/droiddriver/finders/Predicate.java49
-rw-r--r--src/io/appium/droiddriver/finders/Predicates.java321
-rw-r--r--src/io/appium/droiddriver/finders/XPaths.java138
9 files changed, 1208 insertions, 0 deletions
diff --git a/src/io/appium/droiddriver/finders/Attribute.java b/src/io/appium/droiddriver/finders/Attribute.java
new file mode 100644
index 0000000..9dda497
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/Attribute.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+public enum Attribute {
+ CHECKABLE("checkable"),
+ CHECKED("checked"),
+ CLASS("class"),
+ CLICKABLE("clickable"),
+ CONTENT_DESC("content-desc"),
+ ENABLED("enabled"),
+ FOCUSABLE("focusable"),
+ FOCUSED("focused"),
+ LONG_CLICKABLE("long-clickable"),
+ PACKAGE("package"),
+ PASSWORD("password"),
+ RESOURCE_ID("resource-id"),
+ SCROLLABLE("scrollable"),
+ SELECTION_START("selection-start"),
+ SELECTION_END("selection-end"),
+ SELECTED("selected"),
+ TEXT("text"),
+ BOUNDS("bounds");
+
+ private final String name;
+
+ private Attribute(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/io/appium/droiddriver/finders/By.java b/src/io/appium/droiddriver/finders/By.java
new file mode 100644
index 0000000..874cd29
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/By.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+
+import static io.appium.droiddriver.util.Preconditions.checkNotNull;
+
+/**
+ * Convenience methods to create commonly used finders.
+ */
+public class By {
+ private static final MatchFinder ANY = new MatchFinder(null);
+
+ /** Matches any UiElement. */
+ public static MatchFinder any() {
+ return ANY;
+ }
+
+ /** Matches a UiElement whose {@code attribute} is {@code true}. */
+ public static MatchFinder is(Attribute attribute) {
+ return new MatchFinder(Predicates.attributeTrue(attribute));
+ }
+
+ /**
+ * Matches a UiElement whose {@code attribute} is {@code false} or is not set.
+ */
+ public static MatchFinder not(Attribute attribute) {
+ return new MatchFinder(Predicates.attributeFalse(attribute));
+ }
+
+ /** Matches a UiElement by resource id. */
+ public static MatchFinder resourceId(String resourceId) {
+ return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId));
+ }
+
+ /** Matches a UiElement by package name. */
+ public static MatchFinder packageName(String name) {
+ return new MatchFinder(Predicates.attributeEquals(Attribute.PACKAGE, name));
+ }
+
+ /** Matches a UiElement by the exact text. */
+ public static MatchFinder text(String text) {
+ return new MatchFinder(Predicates.attributeEquals(Attribute.TEXT, text));
+ }
+
+ /** Matches a UiElement whose text matches {@code regex}. */
+ public static MatchFinder textRegex(String regex) {
+ return new MatchFinder(Predicates.attributeMatches(Attribute.TEXT, regex));
+ }
+
+ /** Matches a UiElement whose text contains {@code substring}. */
+ public static MatchFinder textContains(String substring) {
+ return new MatchFinder(Predicates.attributeContains(Attribute.TEXT, substring));
+ }
+
+ /** Matches a UiElement by content description. */
+ public static MatchFinder contentDescription(String contentDescription) {
+ return new MatchFinder(Predicates.attributeEquals(Attribute.CONTENT_DESC, contentDescription));
+ }
+
+ /** Matches a UiElement whose content description contains {@code substring}. */
+ public static MatchFinder contentDescriptionContains(String substring) {
+ return new MatchFinder(Predicates.attributeContains(Attribute.CONTENT_DESC, substring));
+ }
+
+ /** Matches a UiElement by class name. */
+ public static MatchFinder className(String className) {
+ return new MatchFinder(Predicates.attributeEquals(Attribute.CLASS, className));
+ }
+
+ /** Matches a UiElement by class name. */
+ public static MatchFinder className(Class<?> clazz) {
+ return className(clazz.getName());
+ }
+
+ /** Matches a UiElement that is selected. */
+ public static MatchFinder selected() {
+ return is(Attribute.SELECTED);
+ }
+
+ /**
+ * Matches by XPath. When applied on an non-root element, it will not evaluate
+ * above the context element.
+ * <p>
+ * XPath is the domain-specific-language for navigating a node tree. It is
+ * ideal if the UiElement to match has a complex relationship with surrounding
+ * nodes. For simple cases, {@link #withParent} or {@link #withAncestor} are
+ * preferred, which can combine with other {@link MatchFinder}s in
+ * {@link #allOf}. For complex cases like below, XPath is superior:
+ *
+ * <pre>
+ * {@code
+ * <View><!-- a custom view to group a cluster of items -->
+ * <LinearLayout>
+ * <TextView text='Albums'/>
+ * <TextView text='4 MORE'/>
+ * </LinearLayout>
+ * <RelativeLayout>
+ * <TextView text='Forever'/>
+ * <ImageView/>
+ * </RelativeLayout>
+ * </View><!-- end of Albums cluster -->
+ * <!-- imagine there are other clusters for Artists and Songs -->
+ * }
+ * </pre>
+ *
+ * If we need to locate the RelativeLayout containing the album "Forever"
+ * instead of a song or an artist named "Forever", this XPath works:
+ *
+ * <pre>
+ * {@code //*[LinearLayout/*[@text='Albums']]/RelativeLayout[*[@text='Forever']]}
+ * </pre>
+ *
+ * @param xPath The xpath to use
+ * @return a finder which locates elements via XPath
+ */
+ public static ByXPath xpath(String xPath) {
+ return new ByXPath(xPath);
+ }
+
+ /**
+ * Returns a finder that uses the UiElement returned by first Finder as
+ * context for the second Finder.
+ * <p>
+ * typically first Finder finds the ancestor, then second Finder finds the
+ * target UiElement, which is a descendant.
+ * </p>
+ * Note that if the first Finder matches multiple UiElements, only the first
+ * match is tried, which usually is not what callers expect. In this case,
+ * allOf(second, withAncesor(first)) may work.
+ */
+ public static ChainFinder chain(Finder first, Finder second) {
+ return new ChainFinder(first, second);
+ }
+
+ private static Predicate<? super UiElement>[] getPredicates(MatchFinder... finders) {
+ @SuppressWarnings("unchecked")
+ Predicate<? super UiElement>[] predicates = new Predicate[finders.length];
+ for (int i = 0; i < finders.length; i++) {
+ predicates[i] = finders[i].predicate;
+ }
+ return predicates;
+ }
+
+ /**
+ * Evaluates given {@code finders} in short-circuit fashion in the order
+ * they are passed. Costly finders (for example those returned by with*
+ * methods that navigate the node tree) should be passed after cheap finders
+ * (for example the ByAttribute finders).
+ *
+ * @return a finder that is the logical conjunction of given finders
+ */
+ public static MatchFinder allOf(final MatchFinder... finders) {
+ return new MatchFinder(Predicates.allOf(getPredicates(finders)));
+ }
+
+ /**
+ * Evaluates given {@code finders} in short-circuit fashion in the order
+ * they are passed. Costly finders (for example those returned by with*
+ * methods that navigate the node tree) should be passed after cheap finders
+ * (for example the ByAttribute finders).
+ *
+ * @return a finder that is the logical disjunction of given finders
+ */
+ public static MatchFinder anyOf(final MatchFinder... finders) {
+ return new MatchFinder(Predicates.anyOf(getPredicates(finders)));
+ }
+
+ /**
+ * Matches a UiElement whose parent matches the given parentFinder. For
+ * complex cases, consider {@link #xpath}.
+ */
+ public static MatchFinder withParent(MatchFinder parentFinder) {
+ checkNotNull(parentFinder);
+ return new MatchFinder(Predicates.withParent(parentFinder.predicate));
+ }
+
+ /**
+ * Matches a UiElement whose ancestor matches the given ancestorFinder. For
+ * complex cases, consider {@link #xpath}.
+ */
+ public static MatchFinder withAncestor(MatchFinder ancestorFinder) {
+ checkNotNull(ancestorFinder);
+ return new MatchFinder(Predicates.withAncestor(ancestorFinder.predicate));
+ }
+
+ /**
+ * Matches a UiElement which has a visible sibling matching the given
+ * siblingFinder. This could be inefficient; consider {@link #xpath}.
+ */
+ public static MatchFinder withSibling(MatchFinder siblingFinder) {
+ checkNotNull(siblingFinder);
+ return new MatchFinder(Predicates.withSibling(siblingFinder.predicate));
+ }
+
+ /**
+ * Matches a UiElement which has a visible child matching the given
+ * childFinder. This could be inefficient; consider {@link #xpath}.
+ */
+ public static MatchFinder withChild(MatchFinder childFinder) {
+ checkNotNull(childFinder);
+ return new MatchFinder(Predicates.withChild(childFinder.predicate));
+ }
+
+ /**
+ * Matches a UiElement whose descendant (including self) matches the given
+ * descendantFinder. This could be VERY inefficient; consider {@link #xpath}.
+ */
+ public static MatchFinder withDescendant(final MatchFinder descendantFinder) {
+ checkNotNull(descendantFinder);
+ return new MatchFinder(new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ try {
+ descendantFinder.find(element);
+ return true;
+ } catch (ElementNotFoundException enfe) {
+ return false;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "withDescendant(" + descendantFinder + ")";
+ }
+ });
+ }
+
+ /** Matches a UiElement that does not match the provided {@code finder}. */
+ public static MatchFinder not(MatchFinder finder) {
+ checkNotNull(finder);
+ return new MatchFinder(Predicates.not(finder.predicate));
+ }
+
+ private By() {}
+}
diff --git a/src/io/appium/droiddriver/finders/ByXPath.java b/src/io/appium/droiddriver/finders/ByXPath.java
new file mode 100644
index 0000000..e230e7f
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/ByXPath.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import android.util.Log;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.BufferedOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.base.BaseUiElement;
+import io.appium.droiddriver.exceptions.DroidDriverException;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+import io.appium.droiddriver.util.FileUtils;
+import io.appium.droiddriver.util.Logs;
+import io.appium.droiddriver.util.Preconditions;
+import io.appium.droiddriver.util.Strings;
+
+/**
+ * Find matching UiElement by XPath.
+ */
+public class ByXPath implements Finder {
+ private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath();
+ // document needs to be static so that when buildDomNode is called recursively
+ // on children they are in the same document to be appended.
+ private static Document document;
+ // The two maps should be kept in sync
+ private static final Map<BaseUiElement<?, ?>, Element> TO_DOM_MAP =
+ new HashMap<BaseUiElement<?, ?>, Element>();
+ private static final Map<Element, BaseUiElement<?, ?>> FROM_DOM_MAP =
+ new HashMap<Element, BaseUiElement<?, ?>>();
+
+ public static void clearData() {
+ TO_DOM_MAP.clear();
+ FROM_DOM_MAP.clear();
+ document = null;
+ }
+
+ private final String xPathString;
+ private final XPathExpression xPathExpression;
+
+ protected ByXPath(String xPathString) {
+ this.xPathString = Preconditions.checkNotNull(xPathString);
+ try {
+ xPathExpression = XPATH_COMPILER.compile(xPathString);
+ } catch (XPathExpressionException e) {
+ throw new DroidDriverException("xPathString=" + xPathString, e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return Strings.toStringHelper(this).addValue(xPathString).toString();
+ }
+
+ @Override
+ public UiElement find(UiElement context) {
+ Element domNode = getDomNode((BaseUiElement<?, ?>) context, UiElement.VISIBLE);
+ try {
+ getDocument().appendChild(domNode);
+ Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE);
+ if (foundNode == null) {
+ Logs.log(Log.DEBUG, "XPath evaluation returns null for " + xPathString);
+ throw new ElementNotFoundException(this);
+ }
+
+ UiElement match = FROM_DOM_MAP.get(foundNode);
+ Logs.log(Log.INFO, "Found match: " + match);
+ return match;
+ } catch (XPathExpressionException e) {
+ throw new ElementNotFoundException(this, e);
+ } finally {
+ try {
+ getDocument().removeChild(domNode);
+ } catch (DOMException e) {
+ Logs.log(Log.ERROR, e, "Failed to clear document");
+ document = null; // getDocument will create new
+ }
+ }
+ }
+
+ private static Document getDocument() {
+ if (document == null) {
+ try {
+ document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+ } catch (ParserConfigurationException e) {
+ throw new DroidDriverException(e);
+ }
+ }
+ return document;
+ }
+
+ /**
+ * Returns the DOM node representing this UiElement.
+ */
+ private static Element getDomNode(BaseUiElement<?, ?> uiElement,
+ Predicate<? super UiElement> predicate) {
+ Element domNode = TO_DOM_MAP.get(uiElement);
+ if (domNode == null) {
+ domNode = buildDomNode(uiElement, predicate);
+ }
+ return domNode;
+ }
+
+ private static Element buildDomNode(BaseUiElement<?, ?> uiElement,
+ Predicate<? super UiElement> predicate) {
+ String className = uiElement.getClassName();
+ if (className == null) {
+ className = "UNKNOWN";
+ }
+ Element element = getDocument().createElement(XPaths.tag(className));
+ TO_DOM_MAP.put(uiElement, element);
+ FROM_DOM_MAP.put(element, uiElement);
+
+ setAttribute(element, Attribute.CLASS, className);
+ setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId());
+ setAttribute(element, Attribute.PACKAGE, uiElement.getPackageName());
+ setAttribute(element, Attribute.CONTENT_DESC, uiElement.getContentDescription());
+ setAttribute(element, Attribute.TEXT, uiElement.getText());
+ setAttribute(element, Attribute.CHECKABLE, uiElement.isCheckable());
+ setAttribute(element, Attribute.CHECKED, uiElement.isChecked());
+ setAttribute(element, Attribute.CLICKABLE, uiElement.isClickable());
+ setAttribute(element, Attribute.ENABLED, uiElement.isEnabled());
+ setAttribute(element, Attribute.FOCUSABLE, uiElement.isFocusable());
+ setAttribute(element, Attribute.FOCUSED, uiElement.isFocused());
+ setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable());
+ setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable());
+ setAttribute(element, Attribute.PASSWORD, uiElement.isPassword());
+ if (uiElement.hasSelection()) {
+ element.setAttribute(Attribute.SELECTION_START.getName(),
+ Integer.toString(uiElement.getSelectionStart()));
+ element.setAttribute(Attribute.SELECTION_END.getName(),
+ Integer.toString(uiElement.getSelectionEnd()));
+ }
+ setAttribute(element, Attribute.SELECTED, uiElement.isSelected());
+ element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString());
+
+ // If we're dumping for debugging, add extra information
+ if (!UiElement.VISIBLE.equals(predicate)) {
+ if (!uiElement.isVisible()) {
+ element.setAttribute(BaseUiElement.ATTRIB_NOT_VISIBLE, "");
+ } else if (!uiElement.getVisibleBounds().equals(uiElement.getBounds())) {
+ element.setAttribute(BaseUiElement.ATTRIB_VISIBLE_BOUNDS, uiElement.getVisibleBounds()
+ .toShortString());
+ }
+ }
+
+ for (BaseUiElement<?, ?> child : uiElement.getChildren(predicate)) {
+ element.appendChild(getDomNode(child, predicate));
+ }
+ return element;
+ }
+
+ private static void setAttribute(Element element, Attribute attr, String value) {
+ if (value != null) {
+ element.setAttribute(attr.getName(), value);
+ }
+ }
+
+ // add attribute only if it's true
+ private static void setAttribute(Element element, Attribute attr, boolean value) {
+ if (value) {
+ element.setAttribute(attr.getName(), "");
+ }
+ }
+
+ public static boolean dumpDom(String path, BaseUiElement<?, ?> uiElement) {
+ BufferedOutputStream bos = null;
+ try {
+ bos = FileUtils.open(path);
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ // find() filters invisible UiElements, but this is for debugging and
+ // invisible UiElements may be of interest.
+ clearData();
+ Element domNode = getDomNode(uiElement, null);
+ transformer.transform(new DOMSource(domNode), new StreamResult(bos));
+ Logs.log(Log.INFO, "Wrote dom to " + path);
+ } catch (Exception e) {
+ Logs.log(Log.ERROR, e, "Failed to transform node");
+ return false;
+ } finally {
+ // We built DOM with invisible UiElements. Don't use it for find()!
+ clearData();
+ if (bos != null) {
+ try {
+ bos.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/io/appium/droiddriver/finders/ChainFinder.java b/src/io/appium/droiddriver/finders/ChainFinder.java
new file mode 100644
index 0000000..492aa22
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/ChainFinder.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.util.Preconditions;
+
+/**
+ * Finds UiElement by applying Finders in turn: using the UiElement returned by
+ * first Finder as context for the second Finder. It is conceptually similar to
+ * <a href="http://en.wikipedia.org/wiki/Functional_composition">Function
+ * composition</a>. The returned UiElement can be thought of as the result of
+ * second(first(context)).
+ * <p>
+ * Note typically first Finder finds the ancestor, then second Finder finds the
+ * target UiElement, which is a descendant. ChainFinder can be chained with
+ * additional Finders to make a "chain".
+ */
+public class ChainFinder implements Finder {
+ private final Finder first;
+ private final Finder second;
+
+ protected ChainFinder(Finder first, Finder second) {
+ this.first = Preconditions.checkNotNull(first);
+ this.second = Preconditions.checkNotNull(second);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Chain{%s, %s}", first, second);
+ }
+
+ @Override
+ public UiElement find(UiElement context) {
+ return second.find(first.find(context));
+ }
+}
diff --git a/src/io/appium/droiddriver/finders/Finder.java b/src/io/appium/droiddriver/finders/Finder.java
new file mode 100644
index 0000000..fef83ae
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/Finder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+
+/**
+ * Interface for finding UiElement.
+ */
+public interface Finder {
+ /**
+ * Returns the matching UiElement. The implementing finder should not poll.
+ * <p>
+ * Invisible UiElements are skipped.
+ *
+ * @param context The starting UiElement, used as search context
+ * @return The first matching element on the current context
+ * @throws ElementNotFoundException If no matching elements are found
+ */
+ UiElement find(UiElement context);
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>
+ * It is recommended that this method return the description of the finder,
+ * for example, "{text=OK}".
+ */
+ @Override
+ String toString();
+}
diff --git a/src/io/appium/droiddriver/finders/MatchFinder.java b/src/io/appium/droiddriver/finders/MatchFinder.java
new file mode 100644
index 0000000..698a915
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/MatchFinder.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import android.util.Log;
+
+import io.appium.droiddriver.UiElement;
+import io.appium.droiddriver.exceptions.ElementNotFoundException;
+import io.appium.droiddriver.util.Logs;
+
+/**
+ * Traverses the UiElement tree and returns the first UiElement satisfying
+ * {@link #predicate}.
+ */
+public class MatchFinder implements Finder {
+ protected final Predicate<? super UiElement> predicate;
+
+ public MatchFinder(Predicate<? super UiElement> predicate) {
+ if (predicate == null) {
+ this.predicate = Predicates.any();
+ } else {
+ this.predicate = predicate;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return predicate.toString();
+ }
+
+ @Override
+ public UiElement find(UiElement context) {
+ if (matches(context)) {
+ Logs.log(Log.INFO, "Found match: " + context);
+ return context;
+ }
+ for (UiElement child : context.getChildren(UiElement.VISIBLE)) {
+ try {
+ return find(child);
+ } catch (ElementNotFoundException enfe) {
+ // Do nothing. Continue searching.
+ }
+ }
+ throw new ElementNotFoundException(this);
+ }
+
+ /**
+ * Returns true if the {@code element} matches this finder. This can be used
+ * to test the exact match of {@code element} when this finder is used in
+ * {@link By#anyOf(MatchFinder...)}.
+ *
+ * @param element The element to validate against
+ * @return true if the element matches
+ */
+ public final boolean matches(UiElement element) {
+ return predicate.apply(element);
+ }
+}
diff --git a/src/io/appium/droiddriver/finders/Predicate.java b/src/io/appium/droiddriver/finders/Predicate.java
new file mode 100644
index 0000000..b33f287
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/Predicate.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+/**
+ * Determines a true or false value for a given input.
+ *
+ * This is replicated from the open-source <a
+ * href="http://guava-libraries.googlecode.com">Guava libraries</a>.
+ * <p>
+ * Many apps use Guava. If a test apk also contains a copy of Guava, duplicated
+ * classes in app and test apks may cause error at run-time:
+ * "Class ref in pre-verified class resolved to unexpected implementation". To
+ * simplify the build and deployment set-up, DroidDriver copies the code of some
+ * Guava classes (often simplified) to this package such that it does not depend
+ * on Guava.
+ * </p>
+ */
+public interface Predicate<T> {
+ /**
+ * Returns the result of applying this predicate to {@code input}.
+ */
+ boolean apply(T input);
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>
+ * It is recommended that this method return the description of this
+ * Predicate.
+ * </p>
+ */
+ @Override
+ String toString();
+}
diff --git a/src/io/appium/droiddriver/finders/Predicates.java b/src/io/appium/droiddriver/finders/Predicates.java
new file mode 100644
index 0000000..1b9ad80
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/Predicates.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import android.text.TextUtils;
+
+import io.appium.droiddriver.UiElement;
+
+/**
+ * Static utility methods pertaining to {@code Predicate} instances.
+ */
+public final class Predicates {
+ private Predicates() {}
+
+ private static final Predicate<Object> ANY = new Predicate<Object>() {
+ @Override
+ public boolean apply(Object o) {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "any";
+ }
+ };
+
+ /**
+ * Returns a predicate that always evaluates to {@code true}.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> any() {
+ return (Predicate<T>) ANY;
+ }
+
+ /**
+ * Returns a predicate that is the negation of the provided {@code predicate}.
+ */
+ public static <T> Predicate<T> not(final Predicate<T> predicate) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T input) {
+ return !predicate.apply(input);
+ }
+
+ @Override
+ public String toString() {
+ return "not(" + predicate + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} if both arguments
+ * evaluate to {@code true}. The arguments are evaluated in order, and
+ * evaluation will be "short-circuited" as soon as a false predicate is found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> allOf(final Predicate<? super T> first,
+ final Predicate<? super T> second) {
+ if (first == null || first == ANY) {
+ return (Predicate<T>) second;
+ }
+ if (second == null || second == ANY) {
+ return (Predicate<T>) first;
+ }
+
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T input) {
+ return first.apply(input) && second.apply(input);
+ }
+
+ @Override
+ public String toString() {
+ return "allOf(" + first + ", " + second + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} if each of its
+ * components evaluates to {@code true}. The components are evaluated in
+ * order, and evaluation will be "short-circuited" as soon as a false
+ * predicate is found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> allOf(final Predicate<? super T>... components) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T input) {
+ for (Predicate<? super T> each : components) {
+ if (!each.apply(input)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "allOf(" + TextUtils.join(", ", components) + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} if any one of its
+ * components evaluates to {@code true}. The components are evaluated in
+ * order, and evaluation will be "short-circuited" as soon as a true predicate
+ * is found.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> anyOf(final Predicate<? super T>... components) {
+ return new Predicate<T>() {
+ @Override
+ public boolean apply(T input) {
+ for (Predicate<? super T> each : components) {
+ if (each.apply(input)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "anyOf(" + TextUtils.join(", ", components) + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+ * if its {@code attribute} is {@code true}.
+ */
+ public static Predicate<UiElement> attributeTrue(final Attribute attribute) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ Boolean actual = element.get(attribute);
+ return actual != null && actual;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%s}", attribute);
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+ * if its {@code attribute} is {@code false}.
+ */
+ public static Predicate<UiElement> attributeFalse(final Attribute attribute) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ Boolean actual = element.get(attribute);
+ return actual == null || !actual;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{not %s}", attribute);
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+ * if its {@code attribute} equals {@code expected}.
+ */
+ public static Predicate<UiElement> attributeEquals(final Attribute attribute,
+ final Object expected) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ Object actual = element.get(attribute);
+ return actual == expected || (actual != null && actual.equals(expected));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%s=%s}", attribute, expected);
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+ * if its {@code attribute} matches {@code regex}.
+ */
+ public static Predicate<UiElement> attributeMatches(final Attribute attribute, final String regex) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ String actual = element.get(attribute);
+ return actual != null && actual.matches(regex);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%s matches %s}", attribute, regex);
+ }
+ };
+ }
+
+ /**
+ * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+ * if its {@code attribute} contains {@code substring}.
+ */
+ public static Predicate<UiElement> attributeContains(final Attribute attribute,
+ final String substring) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ String actual = element.get(attribute);
+ return actual != null && actual.contains(substring);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%s contains %s}", attribute, substring);
+ }
+ };
+ }
+
+ public static Predicate<UiElement> withParent(final Predicate<? super UiElement> parentPredicate) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ UiElement parent = element.getParent();
+ return parent != null && parentPredicate.apply(parent);
+ }
+
+ @Override
+ public String toString() {
+ return "withParent(" + parentPredicate + ")";
+ }
+ };
+ }
+
+ public static Predicate<UiElement> withAncestor(
+ final Predicate<? super UiElement> ancestorPredicate) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ UiElement parent = element.getParent();
+ while (parent != null) {
+ if (ancestorPredicate.apply(parent)) {
+ return true;
+ }
+ parent = parent.getParent();
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "withAncestor(" + ancestorPredicate + ")";
+ }
+ };
+ }
+
+ public static Predicate<UiElement> withSibling(final Predicate<? super UiElement> siblingPredicate) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ UiElement parent = element.getParent();
+ if (parent == null) {
+ return false;
+ }
+ for (UiElement sibling : parent.getChildren(UiElement.VISIBLE)) {
+ if (sibling != element && siblingPredicate.apply(sibling)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "withSibling(" + siblingPredicate + ")";
+ }
+ };
+ }
+
+ public static Predicate<UiElement> withChild(final Predicate<? super UiElement> childPredicate) {
+ return new Predicate<UiElement>() {
+ @Override
+ public boolean apply(UiElement element) {
+ for (UiElement child : element.getChildren(UiElement.VISIBLE)) {
+ if (childPredicate.apply(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "withChild(" + childPredicate + ")";
+ }
+ };
+ }
+}
diff --git a/src/io/appium/droiddriver/finders/XPaths.java b/src/io/appium/droiddriver/finders/XPaths.java
new file mode 100644
index 0000000..31add4e
--- /dev/null
+++ b/src/io/appium/droiddriver/finders/XPaths.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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 io.appium.droiddriver.finders;
+
+import android.text.TextUtils;
+
+/**
+ * Convenience methods and constants for XPath.
+ * <p>
+ * DroidDriver implementation uses default XPath library on device, so the
+ * support may be limited to <a href="http://www.w3.org/TR/xpath/">XPath
+ * 1.0</a>. Newer XPath features may not be supported, for example, the
+ * fn:matches function.
+ */
+public class XPaths {
+
+ private XPaths() {}
+
+ /**
+ * @return The tag name used to build UiElement DOM. It is preferable to use
+ * this to build XPath instead of String literals.
+ */
+ public static String tag(String className) {
+ return simpleClassName(className);
+ }
+
+ /**
+ * @return The tag name used to build UiElement DOM. It is preferable to use
+ * this to build XPath instead of String literals.
+ */
+ public static String tag(Class<?> clazz) {
+ return tag(clazz.getSimpleName());
+ }
+
+ private static String simpleClassName(String name) {
+ // the nth anonymous class has a class name ending in "Outer$n"
+ // and local inner classes have names ending in "Outer.$1Inner"
+ name = name.replaceAll("\\$[0-9]+", "\\$");
+
+ // we want the name of the inner class all by its lonesome
+ int start = name.lastIndexOf('$');
+
+ // if this isn't an inner class, just find the start of the
+ // top level class name.
+ if (start == -1) {
+ start = name.lastIndexOf('.');
+ }
+ return name.substring(start + 1);
+ }
+
+ /**
+ * @return XPath predicate (with enclosing []) for boolean attribute that is
+ * present
+ */
+ public static String is(Attribute attribute) {
+ return "[@" + attribute.getName() + "]";
+ }
+
+ /**
+ * @return XPath predicate (with enclosing []) for boolean attribute that is
+ * NOT present
+ */
+ public static String not(Attribute attribute) {
+ return "[not(@" + attribute.getName() + ")]";
+ }
+
+ /** @return XPath predicate (with enclosing []) for attribute with value */
+ public static String attr(Attribute attribute, String value) {
+ return String.format("[@%s=%s]", attribute.getName(), quoteXPathLiteral(value));
+ }
+
+ /** @return XPath predicate (with enclosing []) for attribute containing value */
+ public static String containsAttr(Attribute attribute, String containedValue) {
+ return String.format("[contains(@%s, %s)]", attribute.getName(),
+ quoteXPathLiteral(containedValue));
+ }
+
+ /** Shorthand for {@link #attr}{@code (Attribute.TEXT, value)} */
+ public static String text(String value) {
+ return attr(Attribute.TEXT, value);
+ }
+
+ /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */
+ public static String resourceId(String value) {
+ return attr(Attribute.RESOURCE_ID, value);
+ }
+
+ /**
+ * @return XPath predicate (with enclosing []) that filters nodes with
+ * descendants satisfying {@code descendantPredicate}.
+ */
+ public static String withDescendant(String descendantPredicate) {
+ return "[.//*" + descendantPredicate + "]";
+ }
+
+ /**
+ * Adapted from http://stackoverflow.com/questions/1341847/.
+ * <p>
+ * Produce an XPath literal equal to the value if possible; if not, produce an
+ * XPath expression that will match the value. Note that this function will
+ * produce very long XPath expressions if a value contains a long run of
+ * double quotes.
+ */
+ static String quoteXPathLiteral(String value) {
+ // if the value contains only single or double quotes, construct an XPath
+ // literal
+ if (!value.contains("\"")) {
+ return "\"" + value + "\"";
+ }
+ if (!value.contains("'")) {
+ return "'" + value + "'";
+ }
+
+ // if the value contains both single and double quotes, construct an
+ // expression that concatenates all non-double-quote substrings with
+ // the quotes, e.g.:
+ // concat("foo", '"', "bar")
+ StringBuilder sb = new StringBuilder();
+ sb.append("concat(\"");
+ sb.append(TextUtils.join("\",'\"',\"", value.split("\"")));
+ sb.append("\")");
+ return sb.toString();
+ }
+}