AccessibilityNodeInfoDumper.java revision 6c66df53c880e480c8016ebf846672b49aa10ec8
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 17package com.android.uiautomator.core; 18 19import android.hardware.display.DisplayManagerGlobal; 20import android.os.Environment; 21import android.os.SystemClock; 22import android.util.Log; 23import android.util.Xml; 24import android.view.Display; 25import android.view.accessibility.AccessibilityNodeInfo; 26 27import java.io.File; 28import java.io.FileWriter; 29import java.io.IOException; 30import java.io.StringWriter; 31 32import org.xmlpull.v1.XmlSerializer; 33 34/** 35 * 36 * @hide 37 */ 38public class AccessibilityNodeInfoDumper { 39 40 private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName(); 41 private static final String[] NAF_EXCLUDED_CLASSES = new String[] { 42 android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(), 43 android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName() 44 }; 45 46 /** 47 * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy 48 * and generates an xml dump into the /data/local/window_dump.xml 49 * @param info 50 */ 51 public static void dumpWindowToFile(AccessibilityNodeInfo info) { 52 File baseDir = new File(Environment.getDataDirectory(), "local"); 53 if (!baseDir.exists()) { 54 baseDir.mkdir(); 55 baseDir.setExecutable(true, false); 56 baseDir.setWritable(true, false); 57 baseDir.setReadable(true, false); 58 } 59 dumpWindowToFile(info, new File( 60 new File(Environment.getDataDirectory(), "local"), "window_dump.xml")); 61 } 62 63 /** 64 * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy 65 * and generates an xml dump to the location specified by <code>dumpFile</code> 66 * @param info 67 */ 68 public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile) { 69 if (root == null) { 70 return; 71 } 72 final long startTime = SystemClock.uptimeMillis(); 73 try { 74 FileWriter writer = new FileWriter(dumpFile); 75 XmlSerializer serializer = Xml.newSerializer(); 76 StringWriter stringWriter = new StringWriter(); 77 serializer.setOutput(stringWriter); 78 serializer.startDocument("UTF-8", true); 79 serializer.startTag("", "hierarchy"); 80 serializer.attribute("", "rotation", Integer.toString( 81 DisplayManagerGlobal.getInstance().getRealDisplay( 82 Display.DEFAULT_DISPLAY).getRotation())); 83 dumpNodeRec(root, serializer, 0); 84 serializer.endTag("", "hierarchy"); 85 serializer.endDocument(); 86 writer.write(stringWriter.toString()); 87 writer.close(); 88 } catch (IOException e) { 89 Log.e(LOGTAG, "failed to dump window to file", e); 90 } 91 final long endTime = SystemClock.uptimeMillis(); 92 Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms"); 93 } 94 95 private static boolean dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer, 96 int index) throws IOException { 97 boolean hasTextOrContentDescription = false; 98 boolean isClickable = node.isClickable(); 99 boolean isEnabled = node.isEnabled(); 100 String textValue = safeCharSeqToString(node.getText()); 101 String descriptionValue = safeCharSeqToString(node.getContentDescription()); 102 if (!textValue.isEmpty() || !descriptionValue.isEmpty()) 103 hasTextOrContentDescription = true; 104 serializer.startTag("", "node"); 105 serializer.attribute("", "index", Integer.toString(index)); 106 serializer.attribute("", "text", textValue); 107 serializer.attribute("", "class", safeCharSeqToString(node.getClassName())); 108 serializer.attribute("", "package", safeCharSeqToString(node.getPackageName())); 109 serializer.attribute("", "content-desc", descriptionValue); 110 serializer.attribute("", "checkable", Boolean.toString(node.isCheckable())); 111 serializer.attribute("", "checked", Boolean.toString(node.isChecked())); 112 serializer.attribute("", "clickable", Boolean.toString(isClickable)); 113 serializer.attribute("", "enabled", Boolean.toString(isEnabled)); 114 serializer.attribute("", "focusable", Boolean.toString(node.isFocusable())); 115 serializer.attribute("", "focused", Boolean.toString(node.isFocused())); 116 serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable())); 117 serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable())); 118 serializer.attribute("", "password", Boolean.toString(node.isPassword())); 119 serializer.attribute("", "selected", Boolean.toString(node.isSelected())); 120 serializer.attribute("", "bounds", 121 AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(node).toShortString()); 122 int count = node.getChildCount(); 123 for (int i = 0; i < count; i++) { 124 AccessibilityNodeInfo child = node.getChild(i); 125 if (child != null) { 126 if (child.isVisibleToUser()) { 127 hasTextOrContentDescription |= dumpNodeRec(child, serializer, i); 128 child.recycle(); 129 } else { 130 Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString())); 131 } 132 } else { 133 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s", 134 i, count, node.toString())); 135 } 136 } 137 // NAF check 138 if (!nafExcludedClass(node) && isClickable && isEnabled && !hasTextOrContentDescription) 139 serializer.attribute("", "NAF", Boolean.toString(true)); 140 141 serializer.endTag("", "node"); 142 return hasTextOrContentDescription; 143 } 144 145 /** 146 * The list of classes to exclude my not be complete. We're attempting to 147 * only reduce noise from standard layout classes that may be falsely 148 * configured to accept clicks and are also enabled. 149 * 150 * @param n 151 * @return 152 */ 153 private static boolean nafExcludedClass(AccessibilityNodeInfo n) { 154 String className = safeCharSeqToString(n.getClassName()); 155 for(String excludedClassName : NAF_EXCLUDED_CLASSES) { 156 if(className.endsWith(excludedClassName)) 157 return true; 158 } 159 return false; 160 } 161 162 private static String safeCharSeqToString(CharSequence cs) { 163 if (cs == null) 164 return ""; 165 else { 166 return stripInvalidXMLChars(cs); 167 } 168 } 169 170 private static String stripInvalidXMLChars(CharSequence cs) { 171 StringBuffer ret = new StringBuffer(); 172 char ch; 173 /* http://www.w3.org/TR/xml11/#charsets 174 [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF], 175 [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF], 176 [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF], 177 [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF], 178 [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF], 179 [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF], 180 [#x10FFFE-#x10FFFF]. 181 */ 182 for (int i = 0; i < cs.length(); i++) { 183 ch = cs.charAt(i); 184 185 if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) || 186 (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) || 187 (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) || 188 (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) || 189 (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) || 190 (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) || 191 (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) || 192 (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) || 193 (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) || 194 (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) || 195 (ch >= 0x10FFFE && ch <= 0x10FFFF)) 196 ret.append("."); 197 else 198 ret.append(ch); 199 } 200 return ret.toString(); 201 } 202} 203