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 
17 package com.android.uiautomator.core;
18 
19 import android.graphics.Point;
20 import android.graphics.Rect;
21 import android.hardware.display.DisplayManagerGlobal;
22 import android.os.Environment;
23 import android.os.SystemClock;
24 import android.util.Log;
25 import android.util.SparseArray;
26 import android.util.Xml;
27 import android.view.Display;
28 import android.view.accessibility.AccessibilityNodeInfo;
29 import android.view.accessibility.AccessibilityWindowInfo;
30 
31 import org.xmlpull.v1.XmlSerializer;
32 
33 import java.io.File;
34 import java.io.FileWriter;
35 import java.io.IOException;
36 import java.io.StringWriter;
37 import java.util.List;
38 
39 /**
40  *
41  * @hide
42  */
43 public class AccessibilityNodeInfoDumper {
44 
45     private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
46     private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
47             android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
48             android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
49     };
50 
51     /**
52      * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
53      * and generates an xml dump into the /data/local/window_dump.xml
54      * @param root The root accessibility node.
55      * @param rotation The rotaion of current display
56      * @param width The pixel width of current display
57      * @param height The pixel height of current display
58      */
dumpWindowToFile(AccessibilityNodeInfo root, int rotation, int width, int height)59     public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
60             int width, int height) {
61         File baseDir = new File(Environment.getDataDirectory(), "local");
62         if (!baseDir.exists()) {
63             baseDir.mkdir();
64             baseDir.setExecutable(true, false);
65             baseDir.setWritable(true, false);
66             baseDir.setReadable(true, false);
67         }
68         dumpWindowToFile(root,
69                 new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
70                 rotation, width, height);
71     }
72 
73     /**
74      * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
75      * and generates an xml dump to the location specified by <code>dumpFile</code>
76      * @param root The root accessibility node.
77      * @param dumpFile The file to dump to.
78      * @param rotation The rotaion of current display
79      * @param width The pixel width of current display
80      * @param height The pixel height of current display
81      */
dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation, int width, int height)82     public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
83             int width, int height) {
84         if (root == null) {
85             return;
86         }
87         final long startTime = SystemClock.uptimeMillis();
88         try {
89             FileWriter writer = new FileWriter(dumpFile);
90             XmlSerializer serializer = Xml.newSerializer();
91             StringWriter stringWriter = new StringWriter();
92             serializer.setOutput(stringWriter);
93             serializer.startDocument("UTF-8", true);
94             serializer.startTag("", "hierarchy");
95             serializer.attribute("", "rotation", Integer.toString(rotation));
96             dumpNodeRec(root, serializer, 0, width, height);
97             serializer.endTag("", "hierarchy");
98             serializer.endDocument();
99             writer.write(stringWriter.toString());
100             writer.close();
101         } catch (IOException e) {
102             Log.e(LOGTAG, "failed to dump window to file", e);
103         }
104         final long endTime = SystemClock.uptimeMillis();
105         Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
106     }
107 
108     /**
109      * Using {@link AccessibilityWindowInfo} this method will dump some window information and
110      * then walk the layout hierarchy of it's
111      * and generates an xml dump to the location specified by <code>dumpFile</code>
112      * @param allWindows All windows indexed by display-id.
113      * @param dumpFile The file to dump to.
114      */
dumpWindowsToFile(SparseArray<List<AccessibilityWindowInfo>> allWindows, File dumpFile, DisplayManagerGlobal displayManager)115     public static void dumpWindowsToFile(SparseArray<List<AccessibilityWindowInfo>> allWindows,
116             File dumpFile, DisplayManagerGlobal displayManager) {
117         if (allWindows.size() == 0) {
118             return;
119         }
120         final long startTime = SystemClock.uptimeMillis();
121         try {
122             FileWriter writer = new FileWriter(dumpFile);
123             XmlSerializer serializer = Xml.newSerializer();
124             StringWriter stringWriter = new StringWriter();
125             serializer.setOutput(stringWriter);
126             serializer.startDocument("UTF-8", true);
127             serializer.startTag("", "displays");
128             for (int d = 0, nd = allWindows.size(); d < nd; ++d) {
129                 int displayId = allWindows.keyAt(d);
130                 Display display = displayManager.getRealDisplay(displayId);
131                 if (display == null) {
132                     continue;
133                 }
134                 final List<AccessibilityWindowInfo> windows = allWindows.valueAt(d);
135                 if (windows.isEmpty()) {
136                     continue;
137                 }
138                 serializer.startTag("", "display");
139                 serializer.attribute("", "id", Integer.toString(displayId));
140                 int rotation = display.getRotation();
141                 Point size = new Point();
142                 display.getRealSize(size);
143                 for (int i = 0, n = windows.size(); i < n; ++i) {
144                     dumpWindowRec(windows.get(i), serializer, i, size.x, size.y, rotation);
145                 }
146                 serializer.endTag("", "display");
147             }
148             serializer.endTag("", "displays");
149             serializer.endDocument();
150             writer.write(stringWriter.toString());
151             writer.close();
152         } catch (IOException e) {
153             Log.e(LOGTAG, "failed to dump window to file", e);
154         }
155         final long endTime = SystemClock.uptimeMillis();
156         Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
157     }
158 
dumpWindowRec(AccessibilityWindowInfo winfo, XmlSerializer serializer, int index, int width, int height, int rotation)159     private static void dumpWindowRec(AccessibilityWindowInfo winfo, XmlSerializer serializer,
160             int index, int width, int height, int rotation) throws IOException {
161         serializer.startTag("", "window");
162         serializer.attribute("", "index", Integer.toString(index));
163         final CharSequence title = winfo.getTitle();
164         serializer.attribute("", "title", title != null ? title.toString() : "");
165         final Rect tmpBounds = new Rect();
166         winfo.getBoundsInScreen(tmpBounds);
167         serializer.attribute("", "bounds", tmpBounds.toShortString());
168         serializer.attribute("", "active", Boolean.toString(winfo.isActive()));
169         serializer.attribute("", "focused", Boolean.toString(winfo.isFocused()));
170         serializer.attribute("", "accessibility-focused",
171                 Boolean.toString(winfo.isAccessibilityFocused()));
172         serializer.attribute("", "id", Integer.toString(winfo.getId()));
173         serializer.attribute("", "layer", Integer.toString(winfo.getLayer()));
174         serializer.attribute("", "type", AccessibilityWindowInfo.typeToString(winfo.getType()));
175         int count = winfo.getChildCount();
176         for (int i = 0; i < count; ++i) {
177             AccessibilityWindowInfo child = winfo.getChild(i);
178             if (child == null) {
179                 Log.i(LOGTAG, String.format("Null window child %d/%d, parent: %s", i, count,
180                         winfo.getTitle()));
181                 continue;
182             }
183             dumpWindowRec(child, serializer, i, width, height, rotation);
184             child.recycle();
185         }
186         AccessibilityNodeInfo root = winfo.getRoot();
187         if (root != null) {
188             serializer.startTag("", "hierarchy");
189             serializer.attribute("", "rotation", Integer.toString(rotation));
190             dumpNodeRec(root, serializer, 0, width, height);
191             root.recycle();
192             serializer.endTag("", "hierarchy");
193         }
194         serializer.endTag("", "window");
195     }
196 
dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index, int width, int height)197     private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
198             int width, int height) throws IOException {
199         serializer.startTag("", "node");
200         if (!nafExcludedClass(node) && !nafCheck(node))
201             serializer.attribute("", "NAF", Boolean.toString(true));
202         serializer.attribute("", "index", Integer.toString(index));
203         serializer.attribute("", "text", safeCharSeqToString(node.getText()));
204         serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
205         serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
206         serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
207         serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
208         serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
209         serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
210         serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
211         serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
212         serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
213         serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
214         serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
215         serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
216         serializer.attribute("", "password", Boolean.toString(node.isPassword()));
217         serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
218         serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
219                 node, width, height).toShortString());
220         int count = node.getChildCount();
221         for (int i = 0; i < count; i++) {
222             AccessibilityNodeInfo child = node.getChild(i);
223             if (child != null) {
224                 if (child.isVisibleToUser()) {
225                     dumpNodeRec(child, serializer, i, width, height);
226                     child.recycle();
227                 } else {
228                     Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
229                 }
230             } else {
231                 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
232                         i, count, node.toString()));
233             }
234         }
235         serializer.endTag("", "node");
236     }
237 
238     /**
239      * The list of classes to exclude my not be complete. We're attempting to
240      * only reduce noise from standard layout classes that may be falsely
241      * configured to accept clicks and are also enabled.
242      *
243      * @param node
244      * @return true if node is excluded.
245      */
nafExcludedClass(AccessibilityNodeInfo node)246     private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
247         String className = safeCharSeqToString(node.getClassName());
248         for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
249             if(className.endsWith(excludedClassName))
250                 return true;
251         }
252         return false;
253     }
254 
255     /**
256      * We're looking for UI controls that are enabled, clickable but have no
257      * text nor content-description. Such controls configuration indicate an
258      * interactive control is present in the UI and is most likely not
259      * accessibility friendly. We refer to such controls here as NAF controls
260      * (Not Accessibility Friendly)
261      *
262      * @param node
263      * @return false if a node fails the check, true if all is OK
264      */
nafCheck(AccessibilityNodeInfo node)265     private static boolean nafCheck(AccessibilityNodeInfo node) {
266         boolean isNaf = node.isClickable() && node.isEnabled()
267                 && safeCharSeqToString(node.getContentDescription()).isEmpty()
268                 && safeCharSeqToString(node.getText()).isEmpty();
269 
270         if (!isNaf)
271             return true;
272 
273         // check children since sometimes the containing element is clickable
274         // and NAF but a child's text or description is available. Will assume
275         // such layout as fine.
276         return childNafCheck(node);
277     }
278 
279     /**
280      * This should be used when it's already determined that the node is NAF and
281      * a further check of its children is in order. A node maybe a container
282      * such as LinerLayout and may be set to be clickable but have no text or
283      * content description but it is counting on one of its children to fulfill
284      * the requirement for being accessibility friendly by having one or more of
285      * its children fill the text or content-description. Such a combination is
286      * considered by this dumper as acceptable for accessibility.
287      *
288      * @param node
289      * @return false if node fails the check.
290      */
childNafCheck(AccessibilityNodeInfo node)291     private static boolean childNafCheck(AccessibilityNodeInfo node) {
292         int childCount = node.getChildCount();
293         for (int x = 0; x < childCount; x++) {
294             AccessibilityNodeInfo childNode = node.getChild(x);
295             if (childNode == null) {
296                 continue;
297             }
298             if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
299                     || !safeCharSeqToString(childNode.getText()).isEmpty()) {
300                 return true;
301             }
302 
303             if (childNafCheck(childNode)) {
304                 return true;
305             }
306         }
307         return false;
308     }
309 
safeCharSeqToString(CharSequence cs)310     private static String safeCharSeqToString(CharSequence cs) {
311         if (cs == null)
312             return "";
313         else {
314             return stripInvalidXMLChars(cs);
315         }
316     }
317 
stripInvalidXMLChars(CharSequence cs)318     private static String stripInvalidXMLChars(CharSequence cs) {
319         StringBuffer ret = new StringBuffer();
320         char ch;
321         /* http://www.w3.org/TR/xml11/#charsets
322         [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
323         [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
324         [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
325         [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
326         [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
327         [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
328         [#x10FFFE-#x10FFFF].
329          */
330         for (int i = 0; i < cs.length(); i++) {
331             ch = cs.charAt(i);
332 
333             if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
334                     (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
335                     (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
336                     (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
337                     (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
338                     (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
339                     (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
340                     (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
341                     (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
342                     (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
343                     (ch >= 0x10FFFE && ch <= 0x10FFFF))
344                 ret.append(".");
345             else
346                 ret.append(ch);
347         }
348         return ret.toString();
349     }
350 }
351