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.os.Environment; 20 import android.os.SystemClock; 21 import android.util.Log; 22 import android.util.Xml; 23 import android.view.accessibility.AccessibilityNodeInfo; 24 25 import org.xmlpull.v1.XmlSerializer; 26 27 import java.io.File; 28 import java.io.FileWriter; 29 import java.io.IOException; 30 import java.io.StringWriter; 31 32 /** 33 * 34 * @hide 35 */ 36 public 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 */ dumpWindowToFile(AccessibilityNodeInfo root, int rotation, int width, int height)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 */ dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation, int width, int height)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 dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index, int width, int height)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 */ nafExcludedClass(AccessibilityNodeInfo node)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 */ nafCheck(AccessibilityNodeInfo node)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 */ childNafCheck(AccessibilityNodeInfo node)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 safeCharSeqToString(CharSequence cs)210 private static String safeCharSeqToString(CharSequence cs) { 211 if (cs == null) 212 return ""; 213 else { 214 return stripInvalidXMLChars(cs); 215 } 216 } 217 stripInvalidXMLChars(CharSequence cs)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