/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* 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.android.bootstrap.utils;
import android.os.Environment;
import android.view.accessibility.AccessibilityNodeInfo;
import io.appium.android.bootstrap.AndroidElement;
import io.appium.android.bootstrap.AndroidElementsHash;
import io.appium.android.bootstrap.exceptions.ElementNotFoundException;
import io.appium.android.bootstrap.exceptions.InvalidSelectorException;
import io.appium.android.bootstrap.exceptions.PairCreationException;
import io.appium.uiautomator.core.AccessibilityNodeInfoDumper;
import io.appium.uiautomator.core.AccessibilityNodeInfoGetter;
import io.appium.uiautomator.core.UiAutomatorBridge;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.xml.parsers.ParserConfigurationException;
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 java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.HashMap;
/**
* Created by jonahss on 8/12/14.
*/
public abstract class XMLHierarchy {
public static ArrayList<ClassInstancePair> getClassInstancePairs(String xpathExpression)
throws ElementNotFoundException, InvalidSelectorException, ParserConfigurationException {
return getClassInstancePairs(compileXpath(xpathExpression), getFormattedXMLDoc());
}
public static ArrayList<ClassInstancePair> getClassInstancePairs(final String xpathExpression, final String contextId)
throws InvalidSelectorException, ElementNotFoundException {
AndroidElement contextElement = AndroidElementsHash.getInstance().getElement(contextId);
AccessibilityNodeInfo contextNode = AccessibilityNodeInfoGetter.fromUiObject(contextElement.getUiObject());
return getClassInstancePairs(compileXpath(xpathExpression), getFormattedXMLDoc(contextNode));
}
private static XPathExpression compileXpath(String xpathExpression) throws InvalidSelectorException {
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression exp = null;
try {
exp = xpath.compile(xpathExpression);
} catch (XPathExpressionException e) {
throw new InvalidSelectorException(e.getMessage());
}
return exp;
}
public static ArrayList<ClassInstancePair> getClassInstancePairs(XPathExpression xpathExpression, Node root) throws ElementNotFoundException {
NodeList nodes;
try {
nodes = (NodeList) xpathExpression.evaluate(root, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
e.printStackTrace();
throw new ElementNotFoundException("XMLWindowHierarchy could not be parsed: " + e.getMessage());
}
ArrayList<ClassInstancePair> pairs = new ArrayList<ClassInstancePair>();
for (int i = 0; i < nodes.getLength(); i++) {
if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
try {
pairs.add(getPairFromNode(nodes.item(i)));
} catch (PairCreationException e) { }
}
}
return pairs;
}
public static InputSource getRawXMLHierarchy() {
AccessibilityNodeInfo root = getRootAccessibilityNode();
return getRawXMLHierarchy(root);
}
public static InputSource getRawXMLHierarchy(AccessibilityNodeInfo root) {
return serializeAccessibilityNode(root);
}
private static AccessibilityNodeInfo getRootAccessibilityNode() {
while(true){
AccessibilityNodeInfo root = UiAutomatorBridge.getInstance().getQueryController().getAccessibilityRootNode();
if (root != null) {
return root;
}
}
}
private static InputSource serializeAccessibilityNode(AccessibilityNodeInfo root) {
try {
final File dumpFolder = new File(Environment.getDataDirectory(), "local/tmp");
final File dumpFile = new File(dumpFolder, "dump.xml");
dumpFolder.mkdirs();
dumpFile.delete();
AccessibilityNodeInfoDumper.dumpWindowToFile(root, dumpFile);
return new InputSource(new FileReader(dumpFile));
} catch (Exception e) {
throw new RuntimeException("Failed to Dump Window Hierarchy", e);
}
}
public static Node getFormattedXMLDoc() {
return formatXMLInput(getRawXMLHierarchy());
}
public static Node getFormattedXMLDoc(AccessibilityNodeInfo root) {
return formatXMLInput(getRawXMLHierarchy(root));
}
public static Node formatXMLInput(InputSource input) {
XPath xpath = XPathFactory.newInstance().newXPath();
Node root = null;
try {
root = (Node) xpath.evaluate("/", input, XPathConstants.NODE);
} catch (XPathExpressionException e) {
throw new RuntimeException("Could not read xml hierarchy: " + e.getMessage());
}
HashMap<String, Integer> instances = new HashMap<String, Integer>();
// rename all the nodes with their "class" attribute
// add an instance attribute
annotateNodes(root, instances);
return root;
}
private static ClassInstancePair getPairFromNode(Node node) throws PairCreationException {
NamedNodeMap attrElements = node.getAttributes();
String androidClass;
String instance;
try {
androidClass = attrElements.getNamedItem("class").getNodeValue();
instance = attrElements.getNamedItem("instance").getNodeValue();
} catch (Exception e) {
throw new PairCreationException("Could not create ClassInstancePair object: " + e.getMessage());
}
return new ClassInstancePair(androidClass, instance);
}
private static void annotateNodes(Node node, HashMap<String, Integer> instances) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
visitNode(children.item(i), instances);
annotateNodes(children.item(i), instances);
}
}
}
// set the node's tag name to the same as it's android class.
// also number all instances of each class with an "instance" number. It increments for each class separately.
// this allows use to use class and instance to identify a node.
// we also take this chance to clean class names that might have dollar signs in them (and other odd characters)
private static void visitNode(Node node, HashMap<String, Integer> instances) {
Document doc = node.getOwnerDocument();
NamedNodeMap attributes = node.getAttributes();
String androidClass;
try {
androidClass = attributes.getNamedItem("class").getNodeValue();
} catch (Exception e) {
return;
}
androidClass = cleanTagName(androidClass);
if (!instances.containsKey(androidClass)) {
instances.put(androidClass, 0);
}
Integer instance = instances.get(androidClass);
Node attrNode = doc.createAttribute("instance");
attrNode.setNodeValue(instance.toString());
attributes.setNamedItem(attrNode);
doc.renameNode(node, node.getNamespaceURI(), androidClass);
instances.put(androidClass, instance + 1);
}
private static String cleanTagName(String name) {
name = name.replaceAll("[$@#&]", ".");
return name.replaceAll("\\s", "");
}
}