Home | History | Annotate | Download | only in core
      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.os.Environment;
     20 import android.os.SystemClock;
     21 import android.util.Log;
     22 import android.util.Xml;
     23 import android.view.accessibility.AccessibilityNodeInfo;
     24 
     25 import org.xmlpull.v1.XmlSerializer;
     26 
     27 import java.io.File;
     28 import java.io.FileWriter;
     29 import java.io.IOException;
     30 import java.io.StringWriter;
     31 
     32 /**
     33  *
     34  * @hide
     35  */
     36 public class AccessibilityNodeInfoDumper {
     37 
     38     private static final String LOGTAG = AccessibilityNodeInfoDumper.class.getSimpleName();
     39     private static final String[] NAF_EXCLUDED_CLASSES = new String[] {
     40             android.widget.GridView.class.getName(), android.widget.GridLayout.class.getName(),
     41             android.widget.ListView.class.getName(), android.widget.TableLayout.class.getName()
     42     };
     43 
     44     /**
     45      * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
     46      * and generates an xml dump into the /data/local/window_dump.xml
     47      * @param root The root accessibility node.
     48      * @param rotation The rotaion of current display
     49      * @param width The pixel width of current display
     50      * @param height The pixel height of current display
     51      */
     52     public static void dumpWindowToFile(AccessibilityNodeInfo root, int rotation,
     53             int width, int height) {
     54         File baseDir = new File(Environment.getDataDirectory(), "local");
     55         if (!baseDir.exists()) {
     56             baseDir.mkdir();
     57             baseDir.setExecutable(true, false);
     58             baseDir.setWritable(true, false);
     59             baseDir.setReadable(true, false);
     60         }
     61         dumpWindowToFile(root,
     62                 new File(new File(Environment.getDataDirectory(), "local"), "window_dump.xml"),
     63                 rotation, width, height);
     64     }
     65 
     66     /**
     67      * Using {@link AccessibilityNodeInfo} this method will walk the layout hierarchy
     68      * and generates an xml dump to the location specified by <code>dumpFile</code>
     69      * @param root The root accessibility node.
     70      * @param dumpFile The file to dump to.
     71      * @param rotation The rotaion of current display
     72      * @param width The pixel width of current display
     73      * @param height The pixel height of current display
     74      */
     75     public static void dumpWindowToFile(AccessibilityNodeInfo root, File dumpFile, int rotation,
     76             int width, int height) {
     77         if (root == null) {
     78             return;
     79         }
     80         final long startTime = SystemClock.uptimeMillis();
     81         try {
     82             FileWriter writer = new FileWriter(dumpFile);
     83             XmlSerializer serializer = Xml.newSerializer();
     84             StringWriter stringWriter = new StringWriter();
     85             serializer.setOutput(stringWriter);
     86             serializer.startDocument("UTF-8", true);
     87             serializer.startTag("", "hierarchy");
     88             serializer.attribute("", "rotation", Integer.toString(rotation));
     89             dumpNodeRec(root, serializer, 0, width, height);
     90             serializer.endTag("", "hierarchy");
     91             serializer.endDocument();
     92             writer.write(stringWriter.toString());
     93             writer.close();
     94         } catch (IOException e) {
     95             Log.e(LOGTAG, "failed to dump window to file", e);
     96         }
     97         final long endTime = SystemClock.uptimeMillis();
     98         Log.w(LOGTAG, "Fetch time: " + (endTime - startTime) + "ms");
     99     }
    100 
    101     private static void dumpNodeRec(AccessibilityNodeInfo node, XmlSerializer serializer,int index,
    102             int width, int height) throws IOException {
    103         serializer.startTag("", "node");
    104         if (!nafExcludedClass(node) && !nafCheck(node))
    105             serializer.attribute("", "NAF", Boolean.toString(true));
    106         serializer.attribute("", "index", Integer.toString(index));
    107         serializer.attribute("", "text", safeCharSeqToString(node.getText()));
    108         serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
    109         serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
    110         serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
    111         serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
    112         serializer.attribute("", "checkable", Boolean.toString(node.isCheckable()));
    113         serializer.attribute("", "checked", Boolean.toString(node.isChecked()));
    114         serializer.attribute("", "clickable", Boolean.toString(node.isClickable()));
    115         serializer.attribute("", "enabled", Boolean.toString(node.isEnabled()));
    116         serializer.attribute("", "focusable", Boolean.toString(node.isFocusable()));
    117         serializer.attribute("", "focused", Boolean.toString(node.isFocused()));
    118         serializer.attribute("", "scrollable", Boolean.toString(node.isScrollable()));
    119         serializer.attribute("", "long-clickable", Boolean.toString(node.isLongClickable()));
    120         serializer.attribute("", "password", Boolean.toString(node.isPassword()));
    121         serializer.attribute("", "selected", Boolean.toString(node.isSelected()));
    122         serializer.attribute("", "bounds", AccessibilityNodeInfoHelper.getVisibleBoundsInScreen(
    123                 node, width, height).toShortString());
    124         int count = node.getChildCount();
    125         for (int i = 0; i < count; i++) {
    126             AccessibilityNodeInfo child = node.getChild(i);
    127             if (child != null) {
    128                 if (child.isVisibleToUser()) {
    129                     dumpNodeRec(child, serializer, i, width, height);
    130                     child.recycle();
    131                 } else {
    132                     Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
    133                 }
    134             } else {
    135                 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
    136                         i, count, node.toString()));
    137             }
    138         }
    139         serializer.endTag("", "node");
    140     }
    141 
    142     /**
    143      * The list of classes to exclude my not be complete. We're attempting to
    144      * only reduce noise from standard layout classes that may be falsely
    145      * configured to accept clicks and are also enabled.
    146      *
    147      * @param node
    148      * @return true if node is excluded.
    149      */
    150     private static boolean nafExcludedClass(AccessibilityNodeInfo node) {
    151         String className = safeCharSeqToString(node.getClassName());
    152         for(String excludedClassName : NAF_EXCLUDED_CLASSES) {
    153             if(className.endsWith(excludedClassName))
    154                 return true;
    155         }
    156         return false;
    157     }
    158 
    159     /**
    160      * We're looking for UI controls that are enabled, clickable but have no
    161      * text nor content-description. Such controls configuration indicate an
    162      * interactive control is present in the UI and is most likely not
    163      * accessibility friendly. We refer to such controls here as NAF controls
    164      * (Not Accessibility Friendly)
    165      *
    166      * @param node
    167      * @return false if a node fails the check, true if all is OK
    168      */
    169     private static boolean nafCheck(AccessibilityNodeInfo node) {
    170         boolean isNaf = node.isClickable() && node.isEnabled()
    171                 && safeCharSeqToString(node.getContentDescription()).isEmpty()
    172                 && safeCharSeqToString(node.getText()).isEmpty();
    173 
    174         if (!isNaf)
    175             return true;
    176 
    177         // check children since sometimes the containing element is clickable
    178         // and NAF but a child's text or description is available. Will assume
    179         // such layout as fine.
    180         return childNafCheck(node);
    181     }
    182 
    183     /**
    184      * This should be used when it's already determined that the node is NAF and
    185      * a further check of its children is in order. A node maybe a container
    186      * such as LinerLayout and may be set to be clickable but have no text or
    187      * content description but it is counting on one of its children to fulfill
    188      * the requirement for being accessibility friendly by having one or more of
    189      * its children fill the text or content-description. Such a combination is
    190      * considered by this dumper as acceptable for accessibility.
    191      *
    192      * @param node
    193      * @return false if node fails the check.
    194      */
    195     private static boolean childNafCheck(AccessibilityNodeInfo node) {
    196         int childCount = node.getChildCount();
    197         for (int x = 0; x < childCount; x++) {
    198             AccessibilityNodeInfo childNode = node.getChild(x);
    199 
    200             if (!safeCharSeqToString(childNode.getContentDescription()).isEmpty()
    201                     || !safeCharSeqToString(childNode.getText()).isEmpty())
    202                 return true;
    203 
    204             if (childNafCheck(childNode))
    205                 return true;
    206         }
    207         return false;
    208     }
    209 
    210     private static String safeCharSeqToString(CharSequence cs) {
    211         if (cs == null)
    212             return "";
    213         else {
    214             return stripInvalidXMLChars(cs);
    215         }
    216     }
    217 
    218     private static String stripInvalidXMLChars(CharSequence cs) {
    219         StringBuffer ret = new StringBuffer();
    220         char ch;
    221         /* http://www.w3.org/TR/xml11/#charsets
    222         [#x1-#x8], [#xB-#xC], [#xE-#x1F], [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF],
    223         [#x1FFFE-#x1FFFF], [#x2FFFE-#x2FFFF], [#x3FFFE-#x3FFFF],
    224         [#x4FFFE-#x4FFFF], [#x5FFFE-#x5FFFF], [#x6FFFE-#x6FFFF],
    225         [#x7FFFE-#x7FFFF], [#x8FFFE-#x8FFFF], [#x9FFFE-#x9FFFF],
    226         [#xAFFFE-#xAFFFF], [#xBFFFE-#xBFFFF], [#xCFFFE-#xCFFFF],
    227         [#xDFFFE-#xDFFFF], [#xEFFFE-#xEFFFF], [#xFFFFE-#xFFFFF],
    228         [#x10FFFE-#x10FFFF].
    229          */
    230         for (int i = 0; i < cs.length(); i++) {
    231             ch = cs.charAt(i);
    232 
    233             if((ch >= 0x1 && ch <= 0x8) || (ch >= 0xB && ch <= 0xC) || (ch >= 0xE && ch <= 0x1F) ||
    234                     (ch >= 0x7F && ch <= 0x84) || (ch >= 0x86 && ch <= 0x9f) ||
    235                     (ch >= 0xFDD0 && ch <= 0xFDDF) || (ch >= 0x1FFFE && ch <= 0x1FFFF) ||
    236                     (ch >= 0x2FFFE && ch <= 0x2FFFF) || (ch >= 0x3FFFE && ch <= 0x3FFFF) ||
    237                     (ch >= 0x4FFFE && ch <= 0x4FFFF) || (ch >= 0x5FFFE && ch <= 0x5FFFF) ||
    238                     (ch >= 0x6FFFE && ch <= 0x6FFFF) || (ch >= 0x7FFFE && ch <= 0x7FFFF) ||
    239                     (ch >= 0x8FFFE && ch <= 0x8FFFF) || (ch >= 0x9FFFE && ch <= 0x9FFFF) ||
    240                     (ch >= 0xAFFFE && ch <= 0xAFFFF) || (ch >= 0xBFFFE && ch <= 0xBFFFF) ||
    241                     (ch >= 0xCFFFE && ch <= 0xCFFFF) || (ch >= 0xDFFFE && ch <= 0xDFFFF) ||
    242                     (ch >= 0xEFFFE && ch <= 0xEFFFF) || (ch >= 0xFFFFE && ch <= 0xFFFFF) ||
    243                     (ch >= 0x10FFFE && ch <= 0x10FFFF))
    244                 ret.append(".");
    245             else
    246                 ret.append(ch);
    247         }
    248         return ret.toString();
    249     }
    250 }
    251