From 663744c839163924037ff1e8c818f8b540675170 Mon Sep 17 00:00:00 2001 From: David Tseng Date: Mon, 6 Jun 2011 13:24:22 -0700 Subject: Initial merge from Eyes-Free/AccessCheck project Change-Id: Ib0cbc361b7de0e0f27c5f636b9cf8b56fbbb8751 --- .../AccessibilityValidationContentHandler.java | 214 +++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java (limited to 'AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java') diff --git a/AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java b/AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java new file mode 100644 index 0000000..77d67f0 --- /dev/null +++ b/AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java @@ -0,0 +1,214 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.android.accessibility; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; + +/** + * An object that handles Android xml layout files in conjunction with an + * XMLParser for the purpose of testing for accessibility based on the following + * rule: + *

+ * If the Element tag is ImageView (or a subclass of ImageView), then the tag + * must contain a contentDescription attribute. + *

+ * This class also has logic to ascertain the subclasses of ImageView and thus + * requires the path to an Android sdk jar. The subclasses are saved for + * application of the above rule when a new XML document tag needs processing. + * + * @author dtseng@google.com (David Tseng) + */ +public class AccessibilityValidationContentHandler extends DefaultHandler { + /** Used to obtain line information within the XML file. */ + private Locator mLocator; + /** The location of the file we are handling. */ + private final String mPath; + /** The total number of errors within the current file. */ + private int mValidationErrors = 0; + + /** + * Element tags we have seen before and determined not to be + * subclasses of ImageView. + */ + private final Set mExclusionList = new HashSet(); + + /** The path to the Android sdk jar file. */ + private final File mAndroidSdkPath; + + /** + * The ImageView class stored for easy comparison while handling content. It + * gets initialized in the {@link AccessibilityValidationHandler} + * constructor if not already done so. + */ + private static Class sImageViewElement; + + /** + * A class loader properly initialized and reusable across files. It gets + * initialized in the {@link AccessibilityValidationHandler} constructor if + * not already done so. + */ + private static ClassLoader sValidationClassLoader; + + /** Attributes we test existence for (for example, contentDescription). */ + private static final HashSet sExpectedAttributes = + new HashSet(); + + /** The object that handles our logging. */ + private static final Logger sLogger = Logger.getLogger("android.accessibility"); + + /** + * Construct an AccessibilityValidationContentHandler object with the file + * on which validation occurs and a path to the Android sdk jar. Then, + * initialize the class members if not previously done so. + * + * @throws IllegalArgumentException + * when given an invalid Android sdk path or when unable to + * locate {@link ImageView} class. + */ + public AccessibilityValidationContentHandler(String fullyQualifiedPath, + File androidSdkPath) throws IllegalArgumentException { + mPath = fullyQualifiedPath; + mAndroidSdkPath = androidSdkPath; + + initializeAccessibilityValidationContentHandler(); + } + + /** + * Used to log line numbers of errors in {@link #startElement}. + */ + @Override + public void setDocumentLocator(Locator locator) { + mLocator = locator; + } + + /** + * For each subclass of ImageView, test for existence of the specified + * attributes. + */ + @Override + public void startElement(String uri, String localName, String qName, + Attributes atts) { + Class potentialClass; + String classPath = "android.widget." + localName; + try { + potentialClass = sValidationClassLoader.loadClass(classPath); + } catch (ClassNotFoundException cnfException) { + return; // do nothing as the class doesn't exist. + } + + // if we already determined this class path isn't a subclass of + // ImageView, skip it. + // Otherwise, check to see if it is a subclass. + if (mExclusionList.contains(classPath)) { + return; + } else if (!sImageViewElement.isAssignableFrom(potentialClass)) { + mExclusionList.add(classPath); + return; + } + + boolean hasAttribute = false; + StringBuilder extendedOutput = new StringBuilder(); + for (int i = 0; i < atts.getLength(); i++) { + String currentAttribute = atts.getLocalName(i).toLowerCase(); + if (sExpectedAttributes.contains(currentAttribute)) { + hasAttribute = true; + break; + } else if (currentAttribute.equals("id")) { + extendedOutput.append("|id=" + currentAttribute); + } else if (currentAttribute.equals("src")) { + extendedOutput.append("|src=" + atts.getValue(i)); + } + } + + if (!hasAttribute) { + if (getValidationErrors() == 0) { + sLogger.info(mPath); + } + sLogger.info(String.format("ln: %s. Error in %s%s tag.", + mLocator.getLineNumber(), localName, extendedOutput)); + mValidationErrors++; + } + } + + /** + * Returns the total number of errors encountered in this file. + */ + public int getValidationErrors() { + return mValidationErrors; + } + + /** + * Set the class loader and ImageView class objects that will be used during + * the startElement validation logic. The class loader encompasses the class + * paths provided. + * + * @throws ClassNotFoundException + * when the ImageView Class object could not be found within the + * provided class loader. + */ + public static void setClassLoaderAndBaseClass(URL[] urlSearchPaths) + throws ClassNotFoundException { + sValidationClassLoader = new URLClassLoader(urlSearchPaths); + sImageViewElement = + sValidationClassLoader.loadClass("android.widget.ImageView"); + } + + /** + * Adds an attribute that will be tested for existence in + * {@link #startElement}. The search will always be case-insensitive. + */ + private static void addExpectedAttribute(String attribute) { + sExpectedAttributes.add(attribute.toLowerCase()); + } + + /** + * Initializes the class loader and {@link ImageView} Class objects. + * + * @throws IllegalArgumentException + * when either an invalid path is provided or ImageView cannot + * be found in the classpaths. + */ + private void initializeAccessibilityValidationContentHandler() + throws IllegalArgumentException { + if (sValidationClassLoader != null && sImageViewElement != null) { + return; // These objects are already initialized. + } + try { + setClassLoaderAndBaseClass(new URL[] { mAndroidSdkPath.toURL() }); + } catch (MalformedURLException mUException) { + throw new IllegalArgumentException("invalid android sdk path", + mUException); + } catch (ClassNotFoundException cnfException) { + throw new IllegalArgumentException( + "Unable to find ImageView class.", cnfException); + } + + // Add all of the expected attributes. + addExpectedAttribute("contentDescription"); + } +} -- cgit v1.2.3