• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         serializer.attribute("", "drawing-order", Integer.toString(node.getDrawingOrder()));
221         serializer.attribute("", "hint", safeCharSeqToString(node.getHintText()));
222 
223         int count = node.getChildCount();
224         for (int i = 0; i < count; i++) {
225             AccessibilityNodeInfo child = node.getChild(i);
226             if (child != null) {
227                 if (child.isVisibleToUser()) {
228                     dumpNodeRec(child, serializer, i, width, height);
229                     child.recycle();
230                 } else {
231                     Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
232                 }
233             } else {
234                 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
235                         i, count, node.toString()));
236             }
237         }
238         serializer.endTag("", "node");
239     }
240 
241     /**
242      * The list of classes to exclude my not be complete. We're attempting to
243      * only reduce noise from standard layout classes that may be falsely
244      * configured to accept clicks and are also enabled.
245      *
246      * @param node
247      * @return true if node is excluded.
248      */
nafExcludedClass(AccessibilityNodeInfo node)249     private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
250         String className = safeCharSeqToString(node.getClassName());
251         for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
252             if(className.endsWith(excludedClassName))
253                 return true;
254         }
255         return false;
256     }
257 
258     /**
259      * We're looking for UI controls that are enabled, clickable but have no
260      * text nor content-description. Such controls configuration indicate an
261      * interactive control is present in the UI and is most likely not
262      * accessibility friendly. We refer to such controls here as NAF controls
263      * (Not Accessibility Friendly)
264      *
265      * @param node
266      * @return false if a node fails the check, true if all is OK
267      */
nafCheck(AccessibilityNodeInfo node)268     private static boolean nafCheck(AccessibilityNodeInfo node) {
269         boolean isNaf = node.isClickable() && node.isEnabled()
270                 && safeCharSeqToString(node.getContentDescription()).isEmpty()
271                 && safeCharSeqToString(node.getText()).isEmpty();
272 
273         if (!isNaf)
274             return true;
275 
276         // check children since sometimes the containing element is clickable
277         // and NAF but a child's text or description is available. Will assume
278         // such layout as fine.
279         return childNafCheck(node);
280     }
281 
282     /**
283      * This should be used when it's already determined that the node is NAF and
284      * a further check of its children is in order. A node maybe a container
285      * such as LinerLayout and may be set to be clickable but have no text or
286      * content description but it is counting on one of its children to fulfill
287      * the requirement for being accessibility friendly by having one or more of
288      * its children fill the text or content-description. Such a combination is
289      * considered by this dumper as acceptable for accessibility.
290      *
291      * @param node
292      * @return false if node fails the check.
293      */
childNafCheck(AccessibilityNodeInfo node)294     private static boolean childNafCheck(AccessibilityNodeInfo node) {
295         int childCount = node.getChildCount();
296         for (int x = 0; x < childCount; x++) {
297             AccessibilityNodeInfo childNode = node.getChild(x);
298             if (childNode == null) {
299                 continue;
300             }
301             if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
302                     || !safeCharSeqToString(childNode.getText()).isEmpty()) {
303                 return true;
304             }
305 
306             if (childNafCheck(childNode)) {
307                 return true;
308             }
309         }
310         return false;
311     }
312 
safeCharSeqToString(CharSequence cs)313     private static String safeCharSeqToString(CharSequence cs) {
314         if (cs == null)
315             return "";
316         else {
317             return stripInvalidXMLChars(cs);
318         }
319     }
320 
stripInvalidXMLChars(CharSequence cs)321     private static String stripInvalidXMLChars(CharSequence cs) {
322         StringBuffer ret = new StringBuffer();
323         char ch;
324         /* http://www.w3.org/TR/xml11/#charsets
325         [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
326         [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
327         [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
328         [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
329         [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
330         [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
331         [#x10FFFE-#x10FFFF].
332          */
333         for (int i = 0; i < cs.length(); i++) {
334             ch = cs.charAt(i);
335 
336             if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
337                     (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
338                     (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
339                     (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
340                     (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
341                     (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
342                     (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
343                     (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
344                     (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
345                     (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
346                     (ch >= 0x10FFFE && ch <= 0x10FFFF))
347                 ret.append(".");
348             else
349                 ret.append(ch);
350         }
351         return ret.toString();
352     }
353 }
354