blob: 77d67f023ba8de2b0db0a60bedf3ce20b1303e4e [file] [log] [blame]
David Tseng663744c2011-06-06 13:24:22 -07001/*
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 */
16package com.android.accessibility;
17
18import org.xml.sax.Attributes;
19import org.xml.sax.Locator;
20import org.xml.sax.helpers.DefaultHandler;
21
22import java.io.File;
23import java.net.MalformedURLException;
24import java.net.URL;
25import java.net.URLClassLoader;
26import java.util.ArrayList;
27import java.util.HashSet;
28import java.util.List;
29import java.util.Set;
30import 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 */
46public 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}