| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.uiautomator.core; |
| |
| import android.graphics.Rect; |
| import android.os.Environment; |
| import android.os.SystemClock; |
| import android.util.Log; |
| import android.util.Xml; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.io.StringWriter; |
| |
| /** |
| * |
| * @hide |
| */ |
| public class AccessibilityNodeInfoDumper { |
| |
| private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName(); |
| private static final String[] EXCLUDED_CLASSES = new String[] |
| {"LinearLayout", "RelativeLayout", "ListView"}; |
| |
| /** |
| * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy |
| * and generates an xml dump into the /data/local/window_dump.xml |
| * @param info |
| */ |
| public static void dumpWindowToFile(AccessibilityNodeInfo info) { |
| File baseDir = new File(Environment.getDataDirectory(), "local"); |
| if (!baseDir.exists()) { |
| baseDir.mkdir(); |
| baseDir.setExecutable(true, false); |
| baseDir.setWritable(true, false); |
| baseDir.setReadable(true, false); |
| } |
| dumpWindowToFile(info, new File( |
| new File(Environment.getDataDirectory(), "local"), "window_dump.xml")); |
| } |
| |
| /** |
| * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy |
| * and generates an xml dump to the location specified by <code>dumpFile</code> |
| * @param info |
| */ |
| public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile) { |
| if (root == null) { |
| return; |
| } |
| final long startTime = SystemClock.uptimeMillis(); |
| try { |
| FileWriter writer = new FileWriter(dumpFile); |
| XmlSerializer serializer = Xml.newSerializer(); |
| StringWriter stringWriter = new StringWriter(); |
| serializer.setOutput(stringWriter); |
| serializer.startDocument("UTF-8", true); |
| serializer.startTag("", "hierarchy"); |
| dumpNodeRec(root, serializer, 0); |
| serializer.endTag("", "hierarchy"); |
| serializer.endDocument(); |
| writer.write(stringWriter.toString()); |
| writer.close(); |
| } catch (IOException e) { |
| Log.e(LOGTAG, "failed to dump window to file", e); |
| } |
| final long endTime = SystemClock.uptimeMillis(); |
| Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms"); |
| } |
| |
| private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer, int index) |
| throws IOException { |
| if(!excludedClass(node) && !accessibilityCheck(node)) { |
| serializer.comment("NAF: The following control may not be accessibility friendly"); |
| serializer.startTag("", "node"); |
| serializer.attribute("", "NAF", Boolean.toString(true)); |
| } else { |
| serializer.startTag("", "node"); |
| } |
| serializer.attribute("", "index", Integer.toString(index)); |
| serializer.attribute("", "text", safeCharSeqToString(node.getText())); |
| serializer.attribute("", "class", safeCharSeqToString(node.getClassName())); |
| serializer.attribute("", "package", safeCharSeqToString(node.getPackageName())); |
| serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription())); |
| serializer.attribute("", "checkable", Boolean.toString(node.isCheckable())); |
| serializer.attribute("", "checked", Boolean.toString(node.isChecked())); |
| serializer.attribute("", "clickable", Boolean.toString(node.isClickable())); |
| serializer.attribute("", "enabled", Boolean.toString(node.isEnabled())); |
| serializer.attribute("", "focusable", Boolean.toString(node.isFocusable())); |
| serializer.attribute("", "focused", Boolean.toString(node.isFocused())); |
| serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable())); |
| serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable())); |
| serializer.attribute("", "password", Boolean.toString(node.isPassword())); |
| serializer.attribute("", "selected", Boolean.toString(node.isSelected())); |
| Rect bounds = new Rect(); |
| node.getBoundsInScreen(bounds); |
| serializer.attribute("", "bounds", bounds.toShortString()); |
| int count = node.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| AccessibilityNodeInfo child = node.getChild(i); |
| if (child != null) { |
| if (child.isVisibleToUser()) { |
| dumpNodeRec(child, serializer, i); |
| child.recycle(); |
| } else { |
| Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString())); |
| } |
| } else { |
| Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s", |
| i, count, node.toString())); |
| } |
| } |
| serializer.endTag("", "node"); |
| } |
| |
| /** |
| * The list of classes to exclude my not be complete. We're attempting to only |
| * reduce noise from standard layout classes that may be falsely configured to |
| * accept clicks and are also enabled. |
| * @param n |
| * @return |
| */ |
| private static boolean excludedClass(AccessibilityNodeInfo n) { |
| String className = safeCharSeqToString(n.getClassName()); |
| for(String excludedClassName : EXCLUDED_CLASSES) { |
| if(className.endsWith(excludedClassName)) |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * We're looking for UI controls that are enabled, clickable but have no text nor |
| * content-description. Such controls configuration indicate an interactive control |
| * is present in the UI and is most likely not accessibility friendly. |
| * @param n |
| * @return |
| */ |
| private static boolean accessibilityCheck(AccessibilityNodeInfo n) { |
| return !(n.isClickable() && n.isEnabled() && |
| safeCharSeqToString(n.getContentDescription()).isEmpty() && |
| safeCharSeqToString(n.getText()).isEmpty()); |
| } |
| |
| private static String safeCharSeqToString(CharSequence cs) { |
| if (cs == null) |
| return ""; |
| else { |
| return stripInvalidXMLChars(cs); |
| } |
| } |
| |
| private static String stripInvalidXMLChars(CharSequence cs) { |
| StringBuffer ret = new StringBuffer(); |
| char ch; |
| /* http://www.w3.org/TR/xml11/#charsets |
| [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF], |
| [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF], |
| [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF], |
| [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF], |
| [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF], |
| [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF], |
| [#x10FFFE-#x10FFFF]. |
| */ |
| for (int i = 0; i < cs.length(); i++) { |
| ch = cs.charAt(i); |
| |
| if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) || |
| (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) || |
| (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) || |
| (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) || |
| (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) || |
| (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) || |
| (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) || |
| (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) || |
| (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) || |
| (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) || |
| (ch >= 0x10FFFE && ch <= 0x10FFFF)) |
| ret.append("."); |
| else |
| ret.append(ch); |
| } |
| return ret.toString(); |
| } |
| } |