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