io.appium.uiautomator2.core.AccessibilityNodeInfoDumper.java Source code

Java tutorial

Introduction

Here is the source code for io.appium.uiautomator2.core.AccessibilityNodeInfoDumper.java

Source

/*
 * Copyright (C) 2012 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 io.appium.uiautomator2.core;

import android.graphics.Point;
import android.os.SystemClock;
import android.util.SparseArray;
import android.util.Xml;
import android.view.Display;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jdom2.Document;
import org.jdom2.filter.Filters;
import org.jdom2.input.JDOMParseException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.xmlpull.v1.XmlSerializer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Semaphore;

import androidx.annotation.Nullable;
import io.appium.uiautomator2.common.exceptions.InvalidSelectorException;
import io.appium.uiautomator2.common.exceptions.UiAutomator2Exception;
import io.appium.uiautomator2.model.NotificationListener;
import io.appium.uiautomator2.model.UiElement;
import io.appium.uiautomator2.model.settings.NormalizeTagNames;
import io.appium.uiautomator2.model.settings.Settings;
import io.appium.uiautomator2.utils.Attribute;
import io.appium.uiautomator2.utils.Logger;
import io.appium.uiautomator2.utils.NodeInfoList;

import static io.appium.uiautomator2.model.UiAutomationElement.rebuildForNewRoot;
import static io.appium.uiautomator2.utils.AXWindowHelpers.currentActiveWindowRoot;
import static io.appium.uiautomator2.utils.XMLHelpers.toNodeName;
import static io.appium.uiautomator2.utils.XMLHelpers.toSafeString;
import static net.gcardone.junidecode.Junidecode.unidecode;

public class AccessibilityNodeInfoDumper {
    // https://github.com/appium/appium/issues/10204
    private static final int MAX_DEPTH = 70;
    private static final String UI_ELEMENT_INDEX = "uiElementIndex";
    private static final String NON_XML_CHAR_REPLACEMENT = "?";
    private static final String NAMESPACE = "";
    private static final String DEFAULT_VIEW_CLASS_NAME = View.class.getName();
    private static final String XML_ENCODING = "UTF-8";
    private static final XPathFactory XPATH = XPathFactory.instance();
    private static final SAXBuilder SAX_BUILDER = new SAXBuilder();
    private final Semaphore RESOURCES_GUARD = new Semaphore(1);

    @Nullable
    private final AccessibilityNodeInfo root;
    @Nullable
    private SparseArray<UiElement<?, ?>> uiElementsMapping = null;
    private boolean shouldAddDisplayInfo;
    private XmlSerializer serializer;

    public AccessibilityNodeInfoDumper() {
        this(null);
    }

    public AccessibilityNodeInfoDumper(@Nullable AccessibilityNodeInfo root) {
        this.root = root;
    }

    private void addDisplayInfo() throws IOException {
        Display display = UiAutomatorBridge.getInstance().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        serializer.attribute(NAMESPACE, "rotation", Integer.toString(display.getRotation()));
        serializer.attribute(NAMESPACE, "width", Integer.toString(size.x));
        serializer.attribute(NAMESPACE, "height", Integer.toString(size.y));
    }

    private static String toXmlNodeName(@Nullable String className) {
        if (StringUtils.isBlank(className)) {
            return DEFAULT_VIEW_CLASS_NAME;
        }

        String fixedName = className.replaceAll("[$@#&]", ".").replaceAll("\\.+", ".").replaceAll("(^\\.|\\.$)",
                "");

        if (((NormalizeTagNames) Settings.NORMALIZE_TAG_NAMES.getSetting()).getValue()) {
            // A workaround for the Apache Harmony bug described in https://github.com/appium/appium/issues/11854
            // The buggy implementation: https://android.googlesource.com/platform/dalvik/+/21d27c095fee51fd6eac6a68d50b79df4dc97d85/libcore/xml/src/main/java/org/apache/harmony/xml/dom/DocumentImpl.java#84
            fixedName = unidecode(fixedName).replaceAll("[^A-Za-z0-9\\-._]", "_");
        }

        fixedName = toNodeName(fixedName);
        if (StringUtils.isBlank(fixedName)) {
            fixedName = DEFAULT_VIEW_CLASS_NAME;
        }
        if (!fixedName.equals(className)) {
            Logger.info(String.format("Rewrote class name '%s' to XML node name '%s'", className, fixedName));
        }
        return fixedName;
    }

    private void serializeUiElement(UiElement<?, ?> uiElement, final int depth) throws IOException {
        final String className = uiElement.getClassName();
        final String nodeName = toXmlNodeName(className);
        serializer.startTag(NAMESPACE, nodeName);

        for (Attribute attr : uiElement.attributeKeys()) {
            if (!attr.isExposableToXml()) {
                continue;
            }
            Object value = uiElement.get(attr);
            if (value == null) {
                continue;
            }
            serializer.attribute(NAMESPACE, attr.getName(),
                    toSafeString(String.valueOf(value), NON_XML_CHAR_REPLACEMENT));
        }
        if (shouldAddDisplayInfo) {
            addDisplayInfo();
            // Display info is only added once to the root node
            shouldAddDisplayInfo = false;
        }

        if (uiElementsMapping != null) {
            final int uiElementIndex = uiElementsMapping.size();
            uiElementsMapping.put(uiElementIndex, uiElement);
            serializer.attribute(NAMESPACE, UI_ELEMENT_INDEX, Integer.toString(uiElementIndex));
        }

        if (depth >= MAX_DEPTH) {
            Logger.error(String.format("The xml tree dump has reached its maximum depth of %s at "
                    + "'%s'. The recursion is stopped to avoid StackOverflowError", MAX_DEPTH, className));
        } else {
            for (UiElement<?, ?> child : uiElement.getChildren()) {
                serializeUiElement(child, depth + 1);
            }
        }
        serializer.endTag(NAMESPACE, nodeName);
    }

    private InputStream toStream() throws IOException {
        final long startTime = SystemClock.uptimeMillis();
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            serializer = Xml.newSerializer();
            shouldAddDisplayInfo = root == null;
            serializer.setOutput(outputStream, XML_ENCODING);
            serializer.startDocument(XML_ENCODING, true);
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            final UiElement<?, ?> xpathRoot = root == null
                    ? rebuildForNewRoot(currentActiveWindowRoot(),
                            NotificationListener.getInstance().getToastMessage())
                    : rebuildForNewRoot(root, null);
            serializeUiElement(xpathRoot, 0);
            serializer.endDocument();
            Logger.debug(String.format("The source XML tree (%s bytes) has been fetched in %sms",
                    outputStream.size(), SystemClock.uptimeMillis() - startTime));
            return new ByteArrayInputStream(outputStream.toByteArray());
        }
    }

    private void performCleanup() {
        uiElementsMapping = null;
    }

    public String dumpToXml() {
        try {
            RESOURCES_GUARD.acquire();
        } catch (InterruptedException e) {
            throw new UiAutomator2Exception(e);
        }
        try (InputStream xmlStream = toStream()) {
            return IOUtils.toString(xmlStream, XML_ENCODING);
        } catch (IOException e) {
            throw new UiAutomator2Exception(e);
        } finally {
            performCleanup();
            RESOURCES_GUARD.release();
        }
    }

    public NodeInfoList findNodes(String xpathSelector, boolean multiple) {
        try {
            XPATH.compile(xpathSelector, Filters.element());
        } catch (IllegalArgumentException e) {
            throw new InvalidSelectorException(e);
        }

        try {
            RESOURCES_GUARD.acquire();
        } catch (InterruptedException e) {
            throw new UiAutomator2Exception(e);
        }
        uiElementsMapping = new SparseArray<>();
        try (InputStream xmlStream = toStream()) {
            final Document document = SAX_BUILDER.build(xmlStream);
            final XPathExpression<org.jdom2.Attribute> expr = XPATH
                    .compile(String.format("(%s)/@%s", xpathSelector, UI_ELEMENT_INDEX), Filters.attribute());
            final NodeInfoList matchedNodes = new NodeInfoList();
            final long timeStarted = SystemClock.uptimeMillis();
            for (org.jdom2.Attribute uiElementId : expr.evaluate(document)) {
                final UiElement uiElement = uiElementsMapping.get(uiElementId.getIntValue());
                if (uiElement == null || uiElement.getNode() == null) {
                    continue;
                }

                matchedNodes.add(uiElement.getNode());
                if (!multiple) {
                    break;
                }
            }
            Logger.debug(String.format("Took %sms to retrieve %s matches for '%s' XPath query",
                    SystemClock.uptimeMillis() - timeStarted, matchedNodes.size(), xpathSelector));
            return matchedNodes;
        } catch (JDOMParseException e) {
            throw new UiAutomator2Exception(String.format(
                    "%s. " + "Try changing the '%s' driver setting to 'true' in order to workaround the problem.",
                    e.getMessage(), Settings.NORMALIZE_TAG_NAMES.toString()), e);
        } catch (Exception e) {
            throw new UiAutomator2Exception(e);
        } finally {
            performCleanup();
            RESOURCES_GUARD.release();
        }
    }
}