[go: nahoru, domu]

blob: b67734a9ed54c465ab1e041d3a70f4ea8a76d662 [file] [log] [blame]
/*
* 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 androidx.test.uiautomator;
import android.os.Build;
import android.util.Log;
import android.util.Xml;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.annotation.DoNotInline;
import androidx.annotation.RequiresApi;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
class AccessibilityNodeInfoDumper {
private AccessibilityNodeInfoDumper() { }
private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
};
public static void dumpWindowHierarchy(UiDevice device, OutputStream out) throws IOException {
XmlSerializer serializer = Xml.newSerializer();
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.setOutput(out, "UTF-8");
serializer.startDocument("UTF-8", true);
serializer.startTag("", "hierarchy"); // TODO(allenhair): Should we use a namespace?
serializer.attribute("", "rotation", Integer.toString(device.getDisplayRotation()));
for (AccessibilityNodeInfo root : device.getWindowRoots()) {
dumpNodeRec(root, serializer, 0, device.getDisplayWidth(), device.getDisplayHeight());
}
serializer.endTag("", "hierarchy");
serializer.endDocument();
}
private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
int width, int height) throws IOException {
serializer.startTag("", "node");
if (!nafExcludedClass(node) && !nafCheck(node))
serializer.attribute("", "NAF", Boolean.toString(true));
serializer.attribute("", "index", Integer.toString(index));
serializer.attribute("", "text", safeCharSeqToString(node.getText()));
serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
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()));
serializer.attribute("", "visible-to-user", Boolean.toString(node.isVisibleToUser()));
serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
node, width, height, false).toShortString());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
serializer.attribute("", "hint", safeCharSeqToString(Api26Impl.getHintText(node)));
}
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, width, height);
child.recycle();
} else {
Log.i(LOGTAG, String.format("Skipping invisible child: %s", child));
}
} else {
Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
i, count, node));
}
}
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 node
* @return true if node is excluded.
*/
private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
String className = safeCharSeqToString(node.getClassName());
for(String excludedClassName : NAF_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. We refer to such controls here as NAF controls
* (Not Accessibility Friendly)
*
* @param node
* @return false if a node fails the check, true if all is OK
*/
private static boolean nafCheck(AccessibilityNodeInfo node) {
boolean isNaf = node.isClickable() && node.isEnabled()
&& safeCharSeqToString(node.getContentDescription()).isEmpty()
&& safeCharSeqToString(node.getText()).isEmpty();
if (!isNaf)
return true;
// check children since sometimes the containing element is clickable
// and NAF but a child's text or description is available. Will assume
// such layout as fine.
return childNafCheck(node);
}
/**
* This should be used when it's already determined that the node is NAF and
* a further check of its children is in order. A node maybe a container
* such as LinerLayout and may be set to be clickable but have no text or
* content description but it is counting on one of its children to fulfill
* the requirement for being accessibility friendly by having one or more of
* its children fill the text or content-description. Such a combination is
* considered by this dumper as acceptable for accessibility.
*
* @param node
* @return false if node fails the check.
*/
private static boolean childNafCheck(AccessibilityNodeInfo node) {
int childCount = node.getChildCount();
for (int x = 0; x < childCount; x++) {
AccessibilityNodeInfo childNode = node.getChild(x);
if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
|| !safeCharSeqToString(childNode.getText()).isEmpty())
return true;
if (childNafCheck(childNode))
return true;
}
return false;
}
private static String safeCharSeqToString(CharSequence cs) {
return cs == null ? "" : stripInvalidXMLChars(cs);
}
private static String stripInvalidXMLChars(CharSequence cs) {
StringBuilder ret = new StringBuilder();
char ch;
for (int i = 0; i < cs.length(); i++) {
ch = cs.charAt(i);
// http://www.w3.org/TR/xml11/#charsets
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)) {
ret.append(".");
} else {
ret.append(ch);
}
}
return ret.toString();
}
@RequiresApi(26)
static class Api26Impl {
private Api26Impl() {
}
@DoNotInline
static String getHintText(AccessibilityNodeInfo accessibilityNodeInfo) {
CharSequence chars = accessibilityNodeInfo.getHintText();
return chars != null ? chars.toString() : null;
}
}
}