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