Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright 2010 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package com.android.accessibility;
     17 
     18 import org.xml.sax.Attributes;
     19 import org.xml.sax.Locator;
     20 import org.xml.sax.helpers.DefaultHandler;
     21 
     22 import java.io.File;
     23 import java.net.MalformedURLException;
     24 import java.net.URL;
     25 import java.net.URLClassLoader;
     26 import java.util.ArrayList;
     27 import java.util.HashSet;
     28 import java.util.List;
     29 import java.util.Set;
     30 import java.util.logging.Logger;
     31 
     32 /**
     33  * An object that handles Android xml layout files in conjunction with an
     34  * XMLParser for the purpose of testing for accessibility based on the following
     35  * rule:
     36  * <p>
     37  * If the Element tag is ImageView (or a subclass of ImageView), then the tag
     38  * must contain a contentDescription attribute.
     39  * <p>
     40  * This class also has logic to ascertain the subclasses of ImageView and thus
     41  * requires the path to an Android sdk jar. The subclasses are saved for
     42  * application of the above rule when a new XML document tag needs processing.
     43  *
     44  * @author dtseng (at) google.com (David Tseng)
     45  */
     46 public class AccessibilityValidationContentHandler extends DefaultHandler {
     47     /** Used to obtain line information within the XML file. */
     48     private Locator mLocator;
     49     /** The location of the file we are handling. */
     50     private final String mPath;
     51     /** The total number of errors within the current file. */
     52     private int mValidationErrors = 0;
     53 
     54     /**
     55      * Element tags we have seen before and determined not to be
     56      * subclasses of ImageView.
     57      */
     58     private final Set<String> mExclusionList = new HashSet<String>();
     59 
     60     /** The path to the Android sdk jar file. */
     61     private final File mAndroidSdkPath;
     62 
     63     /**
     64      * The ImageView class stored for easy comparison while handling content. It
     65      * gets initialized in the {@link AccessibilityValidationHandler}
     66      * constructor if not already done so.
     67      */
     68     private static Class<?> sImageViewElement;
     69 
     70     /**
     71      * A class loader properly initialized and reusable across files. It gets
     72      * initialized in the {@link AccessibilityValidationHandler} constructor if
     73      * not already done so.
     74      */
     75     private static ClassLoader sValidationClassLoader;
     76 
     77     /** Attributes we test existence for (for example, contentDescription). */
     78     private static final HashSet<String> sExpectedAttributes =
     79             new HashSet<String>();
     80 
     81     /** The object that handles our logging. */
     82     private static final Logger sLogger = Logger.getLogger("android.accessibility");
     83 
     84     /**
     85      * Construct an AccessibilityValidationContentHandler object with the file
     86      * on which validation occurs and a path to the Android sdk jar. Then,
     87      * initialize the class members if not previously done so.
     88      *
     89      * @throws IllegalArgumentException
     90      *             when given an invalid Android sdk path or when unable to
     91      *             locate {@link ImageView} class.
     92      */
     93     public AccessibilityValidationContentHandler(String fullyQualifiedPath,
     94             File androidSdkPath) throws IllegalArgumentException {
     95         mPath = fullyQualifiedPath;
     96         mAndroidSdkPath = androidSdkPath;
     97 
     98         initializeAccessibilityValidationContentHandler();
     99     }
    100 
    101     /**
    102      * Used to log line numbers of errors in {@link #startElement}.
    103      */
    104     @Override
    105     public void setDocumentLocator(Locator locator) {
    106         mLocator = locator;
    107     }
    108 
    109     /**
    110      * For each subclass of ImageView, test for existence of the specified
    111      * attributes.
    112      */
    113     @Override
    114     public void startElement(String uri, String localName, String qName,
    115             Attributes atts) {
    116         Class<?> potentialClass;
    117         String classPath = "android.widget." + localName;
    118         try {
    119             potentialClass = sValidationClassLoader.loadClass(classPath);
    120         } catch (ClassNotFoundException cnfException) {
    121             return; // do nothing as the class doesn't exist.
    122         }
    123 
    124         // if we already determined this class path isn't a subclass of
    125         // ImageView, skip it.
    126         // Otherwise, check to see if it is a subclass.
    127         if (mExclusionList.contains(classPath)) {
    128             return;
    129         } else if (!sImageViewElement.isAssignableFrom(potentialClass)) {
    130             mExclusionList.add(classPath);
    131             return;
    132         }
    133 
    134         boolean hasAttribute = false;
    135         StringBuilder extendedOutput = new StringBuilder();
    136         for (int i = 0; i < atts.getLength(); i++) {
    137             String currentAttribute = atts.getLocalName(i).toLowerCase();
    138             if (sExpectedAttributes.contains(currentAttribute)) {
    139                 hasAttribute = true;
    140                 break;
    141             } else if (currentAttribute.equals("id")) {
    142                 extendedOutput.append("|id=" + currentAttribute);
    143             } else if (currentAttribute.equals("src")) {
    144                 extendedOutput.append("|src=" + atts.getValue(i));
    145             }
    146         }
    147 
    148         if (!hasAttribute) {
    149             if (getValidationErrors() == 0) {
    150                 sLogger.info(mPath);
    151             }
    152             sLogger.info(String.format("ln: %s.  Error in %s%s tag.",
    153                     mLocator.getLineNumber(), localName, extendedOutput));
    154             mValidationErrors++;
    155         }
    156     }
    157 
    158     /**
    159      * Returns the total number of errors encountered in this file.
    160      */
    161     public int getValidationErrors() {
    162         return mValidationErrors;
    163     }
    164 
    165     /**
    166      * Set the class loader and ImageView class objects that will be used during
    167      * the startElement validation logic. The class loader encompasses the class
    168      * paths provided.
    169      *
    170      * @throws ClassNotFoundException
    171      *             when the ImageView Class object could not be found within the
    172      *             provided class loader.
    173      */
    174     public static void setClassLoaderAndBaseClass(URL[] urlSearchPaths)
    175             throws ClassNotFoundException {
    176         sValidationClassLoader = new URLClassLoader(urlSearchPaths);
    177         sImageViewElement =
    178             sValidationClassLoader.loadClass("android.widget.ImageView");
    179     }
    180 
    181     /**
    182      * Adds an attribute that will be tested for existence in
    183      * {@link #startElement}. The search will always be case-insensitive.
    184      */
    185     private static void addExpectedAttribute(String attribute) {
    186         sExpectedAttributes.add(attribute.toLowerCase());
    187     }
    188 
    189     /**
    190      * Initializes the class loader and {@link ImageView} Class objects.
    191      *
    192      * @throws IllegalArgumentException
    193      *             when either an invalid path is provided or ImageView cannot
    194      *             be found in the classpaths.
    195      */
    196     private void initializeAccessibilityValidationContentHandler()
    197             throws IllegalArgumentException {
    198         if (sValidationClassLoader != null && sImageViewElement != null) {
    199             return; // These objects are already initialized.
    200         }
    201         try {
    202             setClassLoaderAndBaseClass(new URL[] { mAndroidSdkPath.toURL() });
    203         } catch (MalformedURLException mUException) {
    204             throw new IllegalArgumentException("invalid android sdk path",
    205                     mUException);
    206         } catch (ClassNotFoundException cnfException) {
    207             throw new IllegalArgumentException(
    208                     "Unable to find ImageView class.", cnfException);
    209         }
    210 
    211         // Add all of the expected attributes.
    212         addExpectedAttribute("contentDescription");
    213     }
    214 }
    215