/* * 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 com.google.android.droiddriver.finders; import android.util.Log; import com.google.android.droiddriver.UiElement; import com.google.android.droiddriver.base.BaseUiElement; import com.google.android.droiddriver.exceptions.DroidDriverException; import com.google.android.droiddriver.exceptions.ElementNotFoundException; import com.google.android.droiddriver.util.FileUtils; import com.google.android.droiddriver.util.Logs; import com.google.android.droiddriver.util.Preconditions; import com.google.android.droiddriver.util.Strings; 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; /** * 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, Element> TO_DOM_MAP = new HashMap, Element>(); private static final Map> FROM_DOM_MAP = new HashMap>(); 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 predicate) { Element domNode = TO_DOM_MAP.get(uiElement); if (domNode == null) { domNode = buildDomNode(uiElement, predicate); } return domNode; } private static Element buildDomNode(BaseUiElement uiElement, Predicate 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; } }