1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.uiautomator.core;
18
19import android.os.Environment;
20import android.os.SystemClock;
21import android.util.Log;
22import android.util.Xml;
23import android.view.accessibility.AccessibilityNodeInfo;
24
25import org.xmlpull.v1.XmlSerializer;
26
27import java.io.File;
28import java.io.FileWriter;
29import java.io.IOException;
30import java.io.StringWriter;
31
32/**
33 *
34 * @hide
35 */
36public class AccessibilityNodeInfoDumper {
37
38    private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
39    private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
40            android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
41            android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
42    };
43
44    /**
45     * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
46     * and generates an xml dump to the location specified by <code>dumpFile</code>
47     * @param root The root accessibility node.
48     * @param dumpFile The file to dump to.
49     * @param rotation The rotaion of current display
50     * @param width The pixel width of current display
51     * @param height The pixel height of current display
52     */
53    public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
54            int width, int height) {
55        if (root == null) {
56            return;
57        }
58        final long startTime = SystemClock.uptimeMillis();
59        try {
60            FileWriter writer = new FileWriter(dumpFile);
61            XmlSerializer serializer = Xml.newSerializer();
62            StringWriter stringWriter = new StringWriter();
63            serializer.setOutput(stringWriter);
64            serializer.startDocument("UTF-8", true);
65            serializer.startTag("", "hierarchy");
66            serializer.attribute("", "rotation", Integer.toString(rotation));
67            dumpNodeRec(root, serializer, 0, width, height);
68            serializer.endTag("", "hierarchy");
69            serializer.endDocument();
70            writer.write(stringWriter.toString());
71            writer.close();
72        } catch (IOException e) {
73            Log.e(LOGTAG, "failed to dump window to file", e);
74        }
75        final long endTime = SystemClock.uptimeMillis();
76        Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
77    }
78
79    private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
80            int width, int height) throws IOException {
81        serializer.startTag("", "node");
82        if (!nafExcludedClass(node) && !nafCheck(node))
83            serializer.attribute("", "NAF", Boolean.toString(true));
84        serializer.attribute("", "index", Integer.toString(index));
85        serializer.attribute("", "text", safeCharSeqToString(node.getText()));
86        serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
87        serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
88        serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
89        serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
90        serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
91        serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
92        serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
93        serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
94        serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
95        serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
96        serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
97        serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
98        serializer.attribute("", "password", Boolean.toString(node.isPassword()));
99        serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
100        serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
101                node, width, height).toShortString());
102        int count = node.getChildCount();
103        for (int i = 0; i < count; i++) {
104            AccessibilityNodeInfo child = node.getChild(i);
105            if (child != null) {
106                if (child.isVisibleToUser()) {
107                    dumpNodeRec(child, serializer, i, width, height);
108                    child.recycle();
109                } else {
110                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
111                }
112            } else {
113                Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
114                        i, count, node.toString()));
115            }
116        }
117        serializer.endTag("", "node");
118    }
119
120    /**
121     * The list of classes to exclude my not be complete. We're attempting to
122     * only reduce noise from standard layout classes that may be falsely
123     * configured to accept clicks and are also enabled.
124     *
125     * @param node
126     * @return true if node is excluded.
127     */
128    private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
129        String className = safeCharSeqToString(node.getClassName());
130        for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
131            if(className.endsWith(excludedClassName))
132                return true;
133        }
134        return false;
135    }
136
137    /**
138     * We're looking for UI controls that are enabled, clickable but have no
139     * text nor content-description. Such controls configuration indicate an
140     * interactive control is present in the UI and is most likely not
141     * accessibility friendly. We refer to such controls here as NAF controls
142     * (Not Accessibility Friendly)
143     *
144     * @param node
145     * @return false if a node fails the check, true if all is OK
146     */
147    private static boolean nafCheck(AccessibilityNodeInfo node) {
148        boolean isNaf = node.isClickable() && node.isEnabled()
149                && safeCharSeqToString(node.getContentDescription()).isEmpty()
150                && safeCharSeqToString(node.getText()).isEmpty();
151
152        if (!isNaf)
153            return true;
154
155        // check children since sometimes the containing element is clickable
156        // and NAF but a child's text or description is available. Will assume
157        // such layout as fine.
158        return childNafCheck(node);
159    }
160
161    /**
162     * This should be used when it's already determined that the node is NAF and
163     * a further check of its children is in order. A node maybe a container
164     * such as LinerLayout and may be set to be clickable but have no text or
165     * content description but it is counting on one of its children to fulfill
166     * the requirement for being accessibility friendly by having one or more of
167     * its children fill the text or content-description. Such a combination is
168     * considered by this dumper as acceptable for accessibility.
169     *
170     * @param node
171     * @return false if node fails the check.
172     */
173    private static boolean childNafCheck(AccessibilityNodeInfo node) {
174        int childCount = node.getChildCount();
175        for (int x = 0; x < childCount; x++) {
176            AccessibilityNodeInfo childNode = node.getChild(x);
177
178            if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
179                    || !safeCharSeqToString(childNode.getText()).isEmpty())
180                return true;
181
182            if (childNafCheck(childNode))
183                return true;
184        }
185        return false;
186    }
187
188    private static String safeCharSeqToString(CharSequence cs) {
189        if (cs == null)
190            return "";
191        else {
192            return stripInvalidXMLChars(cs);
193        }
194    }
195
196    private static String stripInvalidXMLChars(CharSequence cs) {
197        StringBuffer ret = new StringBuffer();
198        char ch;
199        /* http://www.w3.org/TR/xml11/#charsets
200        [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
201        [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
202        [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
203        [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
204        [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
205        [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
206        [#x10FFFE-#x10FFFF].
207         */
208        for (int i = 0; i < cs.length(); i++) {
209            ch = cs.charAt(i);
210
211            if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
212                    (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
213                    (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
214                    (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
215                    (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
216                    (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
217                    (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
218                    (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
219                    (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
220                    (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
221                    (ch >= 0x10FFFE && ch <= 0x10FFFF))
222                ret.append(".");
223            else
224                ret.append(ch);
225        }
226        return ret.toString();
227    }
228}
229