AccessibilityNodeInfoDumper.java revision 6c66df53c880e480c8016ebf846672b49aa10ec8
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.hardware.display.DisplayManagerGlobal;
20import android.os.Environment;
21import android.os.SystemClock;
22import android.util.Log;
23import android.util.Xml;
24import android.view.Display;
25import android.view.accessibility.AccessibilityNodeInfo;
26
27import java.io.File;
28import java.io.FileWriter;
29import java.io.IOException;
30import java.io.StringWriter;
31
32import org.xmlpull.v1.XmlSerializer;
33
34/**
35 *
36 * @hide
37 */
38public class AccessibilityNodeInfoDumper {
39
40    private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
41    private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
42            android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
43            android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
44    };
45
46    /**
47     * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
48     * and generates an xml dump into the /data/local/window_dump.xml
49     * @param info
50     */
51    public static void dumpWindowToFile(AccessibilityNodeInfo info) {
52        File baseDir = new File(Environment.getDataDirectory(), "local");
53        if (!baseDir.exists()) {
54            baseDir.mkdir();
55            baseDir.setExecutable(true, false);
56            baseDir.setWritable(true, false);
57            baseDir.setReadable(true, false);
58        }
59        dumpWindowToFile(info, new File(
60                new File(Environment.getDataDirectory(), "local"), "window_dump.xml"));
61    }
62
63    /**
64     * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
65     * and generates an xml dump to the location specified by <code>dumpFile</code>
66     * @param info
67     */
68    public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile) {
69        if (root == null) {
70            return;
71        }
72        final long startTime = SystemClock.uptimeMillis();
73        try {
74            FileWriter writer = new FileWriter(dumpFile);
75            XmlSerializer serializer = Xml.newSerializer();
76            StringWriter stringWriter = new StringWriter();
77            serializer.setOutput(stringWriter);
78            serializer.startDocument("UTF-8", true);
79            serializer.startTag("", "hierarchy");
80            serializer.attribute("", "rotation", Integer.toString(
81                    DisplayManagerGlobal.getInstance().getRealDisplay(
82                            Display.DEFAULT_DISPLAY).getRotation()));
83            dumpNodeRec(root, serializer, 0);
84            serializer.endTag("", "hierarchy");
85            serializer.endDocument();
86            writer.write(stringWriter.toString());
87            writer.close();
88        } catch (IOException e) {
89            Log.e(LOGTAG, "failed to dump window to file", e);
90        }
91        final long endTime = SystemClock.uptimeMillis();
92        Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
93    }
94
95    private static boolean dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,
96            int index) throws IOException {
97        boolean hasTextOrContentDescription = false;
98        boolean isClickable = node.isClickable();
99        boolean isEnabled = node.isEnabled();
100        String textValue = safeCharSeqToString(node.getText());
101        String descriptionValue = safeCharSeqToString(node.getContentDescription());
102        if (!textValue.isEmpty() || !descriptionValue.isEmpty())
103            hasTextOrContentDescription = true;
104        serializer.startTag("", "node");
105        serializer.attribute("", "index", Integer.toString(index));
106        serializer.attribute("", "text", textValue);
107        serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
108        serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
109        serializer.attribute("", "content-desc", descriptionValue);
110        serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
111        serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
112        serializer.attribute("", "clickable", Boolean.toString(isClickable));
113        serializer.attribute("", "enabled", Boolean.toString(isEnabled));
114        serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
115        serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
116        serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
117        serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
118        serializer.attribute("", "password", Boolean.toString(node.isPassword()));
119        serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
120        serializer.attribute("", "bounds",
121                AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node).toShortString());
122        int count = node.getChildCount();
123        for (int i = 0; i < count; i++) {
124            AccessibilityNodeInfo child = node.getChild(i);
125            if (child != null) {
126                if (child.isVisibleToUser()) {
127                    hasTextOrContentDescription |= dumpNodeRec(child, serializer, i);
128                    child.recycle();
129                } else {
130                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
131                }
132            } else {
133                Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
134                        i, count, node.toString()));
135            }
136        }
137        // NAF check
138        if (!nafExcludedClass(node) && isClickable && isEnabled && !hasTextOrContentDescription)
139            serializer.attribute("", "NAF", Boolean.toString(true));
140
141        serializer.endTag("", "node");
142        return hasTextOrContentDescription;
143    }
144
145    /**
146     * The list of classes to exclude my not be complete. We're attempting to
147     * only reduce noise from standard layout classes that may be falsely
148     * configured to accept clicks and are also enabled.
149     *
150     * @param n
151     * @return
152     */
153    private static boolean nafExcludedClass(AccessibilityNodeInfo n) {
154        String className = safeCharSeqToString(n.getClassName());
155        for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
156            if(className.endsWith(excludedClassName))
157                return true;
158        }
159        return false;
160    }
161
162    private static String safeCharSeqToString(CharSequence cs) {
163        if (cs == null)
164            return "";
165        else {
166            return stripInvalidXMLChars(cs);
167        }
168    }
169
170    private static String stripInvalidXMLChars(CharSequence cs) {
171        StringBuffer ret = new StringBuffer();
172        char ch;
173        /* http://www.w3.org/TR/xml11/#charsets
174        [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
175        [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
176        [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
177        [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
178        [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
179        [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
180        [#x10FFFE-#x10FFFF].
181         */
182        for (int i = 0; i < cs.length(); i++) {
183            ch = cs.charAt(i);
184
185            if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
186                    (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
187                    (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
188                    (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
189                    (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
190                    (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
191                    (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
192                    (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
193                    (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
194                    (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
195                    (ch >= 0x10FFFE && ch <= 0x10FFFF))
196                ret.append(".");
197            else
198                ret.append(ch);
199        }
200        return ret.toString();
201    }
202}
203