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@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      */
AccessibilityValidationContentHandler(String fullyQualifiedPath, File androidSdkPath)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
setDocumentLocator(Locator locator)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
startElement(String uri, String localName, String qName, Attributes atts)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      */
getValidationErrors()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      */
setClassLoaderAndBaseClass(URL[] urlSearchPaths)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      */
addExpectedAttribute(String attribute)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      */
initializeAccessibilityValidationContentHandler()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