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 into the /data/local/window_dump.xml
47     * @param root The root accessibility node.
48     * @param rotation The rotaion of current display
49     * @param width The pixel width of current display
50     * @param height The pixel height of current display
51     */
52    public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
53            int width, int height) {
54        File baseDir = new File(Environment.getDataDirectory(), "local");
55        if (!baseDir.exists()) {
56            baseDir.mkdir();
57            baseDir.setExecutable(true, false);
58            baseDir.setWritable(true, false);
59            baseDir.setReadable(true, false);
60        }
61        dumpWindowToFile(root,
62                new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
63                rotation, width, height);
64    }
65
66    /**
67     * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
68     * and generates an xml dump to the location specified by <code>dumpFile</code>
69     * @param root The root accessibility node.
70     * @param dumpFile The file to dump to.
71     * @param rotation The rotaion of current display
72     * @param width The pixel width of current display
73     * @param height The pixel height of current display
74     */
75    public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
76            int width, int height) {
77        if (root == null) {
78            return;
79        }
80        final long startTime = SystemClock.uptimeMillis();
81        try {
82            FileWriter writer = new FileWriter(dumpFile);
83            XmlSerializer serializer = Xml.newSerializer();
84            StringWriter stringWriter = new StringWriter();
85            serializer.setOutput(stringWriter);
86            serializer.startDocument("UTF-8", true);
87            serializer.startTag("", "hierarchy");
88            serializer.attribute("", "rotation", Integer.toString(rotation));
89            dumpNodeRec(root, serializer, 0, width, height);
90            serializer.endTag("", "hierarchy");
91            serializer.endDocument();
92            writer.write(stringWriter.toString());
93            writer.close();
94        } catch (IOException e) {
95            Log.e(LOGTAG, "failed to dump window to file", e);
96        }
97        final long endTime = SystemClock.uptimeMillis();
98        Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
99    }
100
101    private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
102            int width, int height) throws IOException {
103        serializer.startTag("", "node");
104        if (!nafExcludedClass(node) && !nafCheck(node))
105            serializer.attribute("", "NAF", Boolean.toString(true));
106        serializer.attribute("", "index", Integer.toString(index));
107        serializer.attribute("", "text", safeCharSeqToString(node.getText()));
108        serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
109        serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
110        serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
111        serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
112        serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
113        serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
114        serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
115        serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
116        serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
117        serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
118        serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
119        serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
120        serializer.attribute("", "password", Boolean.toString(node.isPassword()));
121        serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
122        serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
123                node, width, height).toShortString());
124        int count = node.getChildCount();
125        for (int i = 0; i < count; i++) {
126            AccessibilityNodeInfo child = node.getChild(i);
127            if (child != null) {
128                if (child.isVisibleToUser()) {
129                    dumpNodeRec(child, serializer, i, width, height);
130                    child.recycle();
131                } else {
132                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
133                }
134            } else {
135                Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
136                        i, count, node.toString()));
137            }
138        }
139        serializer.endTag("", "node");
140    }
141
142    /**
143     * The list of classes to exclude my not be complete. We're attempting to
144     * only reduce noise from standard layout classes that may be falsely
145     * configured to accept clicks and are also enabled.
146     *
147     * @param node
148     * @return true if node is excluded.
149     */
150    private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
151        String className = safeCharSeqToString(node.getClassName());
152        for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
153            if(className.endsWith(excludedClassName))
154                return true;
155        }
156        return false;
157    }
158
159    /**
160     * We're looking for UI controls that are enabled, clickable but have no
161     * text nor content-description. Such controls configuration indicate an
162     * interactive control is present in the UI and is most likely not
163     * accessibility friendly. We refer to such controls here as NAF controls
164     * (Not Accessibility Friendly)
165     *
166     * @param node
167     * @return false if a node fails the check, true if all is OK
168     */
169    private static boolean nafCheck(AccessibilityNodeInfo node) {
170        boolean isNaf = node.isClickable() && node.isEnabled()
171                && safeCharSeqToString(node.getContentDescription()).isEmpty()
172                && safeCharSeqToString(node.getText()).isEmpty();
173
174        if (!isNaf)
175            return true;
176
177        // check children since sometimes the containing element is clickable
178        // and NAF but a child's text or description is available. Will assume
179        // such layout as fine.
180        return childNafCheck(node);
181    }
182
183    /**
184     * This should be used when it's already determined that the node is NAF and
185     * a further check of its children is in order. A node maybe a container
186     * such as LinerLayout and may be set to be clickable but have no text or
187     * content description but it is counting on one of its children to fulfill
188     * the requirement for being accessibility friendly by having one or more of
189     * its children fill the text or content-description. Such a combination is
190     * considered by this dumper as acceptable for accessibility.
191     *
192     * @param node
193     * @return false if node fails the check.
194     */
195    private static boolean childNafCheck(AccessibilityNodeInfo node) {
196        int childCount = node.getChildCount();
197        for (int x = 0; x < childCount; x++) {
198            AccessibilityNodeInfo childNode = node.getChild(x);
199
200            if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
201                    || !safeCharSeqToString(childNode.getText()).isEmpty())
202                return true;
203
204            if (childNafCheck(childNode))
205                return true;
206        }
207        return false;
208    }
209
210    private static String safeCharSeqToString(CharSequence cs) {
211        if (cs == null)
212            return "";
213        else {
214            return stripInvalidXMLChars(cs);
215        }
216    }
217
218    private static String stripInvalidXMLChars(CharSequence cs) {
219        StringBuffer ret = new StringBuffer();
220        char ch;
221        /* http://www.w3.org/TR/xml11/#charsets
222        [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
223        [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
224        [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
225        [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
226        [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
227        [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
228        [#x10FFFE-#x10FFFF].
229         */
230        for (int i = 0; i < cs.length(); i++) {
231            ch = cs.charAt(i);
232
233            if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
234                    (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
235                    (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
236                    (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
237                    (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
238                    (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
239                    (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
240                    (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
241                    (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
242                    (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
243                    (ch >= 0x10FFFE && ch <= 0x10FFFF))
244                ret.append(".");
245            else
246                ret.append(ch);
247        }
248        return ret.toString();
249    }
250}
251