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