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 +++++++++++++++++++++ .../accessibility/AccessibilityValidator.java | 191 ++++++++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java create mode 100644 AccessCheck/src/com/android/accessibility/AccessibilityValidator.java (limited to 'AccessCheck/src/com/android') 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"); + } +} diff --git a/AccessCheck/src/com/android/accessibility/AccessibilityValidator.java b/AccessCheck/src/com/android/accessibility/AccessibilityValidator.java new file mode 100644 index 0000000..5f3b031 --- /dev/null +++ b/AccessCheck/src/com/android/accessibility/AccessibilityValidator.java @@ -0,0 +1,191 @@ +/* + * 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.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * An object that fetches all Android layout files and manages the testing of + * the files with the use of the AccessibilityValidationContentHandler. This + * object also reports on any errors encountered during the testing. + * + * @author dtseng@google.com (David Tseng) + */ +public class AccessibilityValidator { + /** The root path to scan for Android layout files. */ + private final File mRootFilePath; + /** Errors generated by thrown exceptions (and not by validation errors). */ + private final List mGeneralErrors = new ArrayList(); + /** A list of files we wish to have tested. */ + private List mLayoutFiles; + /** The total number of validation test errors across all files. */ + private int mTotalValidationErrors = 0; + /** The path to the Android sdk jar. */ + private final File mAndroidSdkPath; + + /** The object that handles our logging. */ + private static final Logger sLogger = Logger.getLogger("android.accessibility"); + + /** + * The entry point to this tool. + * + * @param + * path on which to search for layout xml files that need + * validation + */ + public static void main(String[] args) { + sLogger.info("AccessibilityValidator"); + if (args.length == 2) { + sLogger.info("Validating classes using android jar for subclasses of ImageView"); + new AccessibilityValidator(args[0], args[1]).run(); + } else { + sLogger.info("Usage: java AccessibilityValidator "); + return; + } + } + + /** + * Constructs an AccessibilityValidator object using the root path and the + * android jar path. + */ + public AccessibilityValidator(String rootPath, String androidSdkPath) { + mRootFilePath = new File(rootPath); + mAndroidSdkPath = new File(androidSdkPath); + + if (!mRootFilePath.exists()) { + throw new IllegalArgumentException("Invalid root path specified " + + rootPath); + } else if (!mAndroidSdkPath.exists()) { + throw new IllegalArgumentException( + "Invalid android sdk path specified " + androidSdkPath); + } + } + + /** + * Performs validation of Android layout files and logs errors that have + * been encountered during the validation. Returns true if the validation + * passes. + */ + private boolean run() { + sLogger.info("Validating files under " + mRootFilePath); + mLayoutFiles = findLayoutFiles(mRootFilePath); + validateFiles(); + for (String error : mGeneralErrors) { + sLogger.info(error); + } + sLogger.info("done with validation"); + return mGeneralErrors.size() == 0; + } + + /** + * Accumulates a list of files under the path that meet two constraints. + * Firstly, it has a containing (parent) directory of "layout". Secondly, it + * has an xml extension + */ + private List findLayoutFiles(File directory) { + List layoutFiles = new ArrayList(); + + for (File file : directory.listFiles()) { + // The file is a directory; recurse on the file. + if (file.isDirectory()) { + List directoryFiles = findLayoutFiles(file); + layoutFiles.addAll(directoryFiles); + // Does the containing directory and filename meet our + // constraints? + } else if (directory.getName().toLowerCase().contains("layout") + && file.getName().toLowerCase().endsWith(".xml")) { + InputSource addition; + try { + addition = new InputSource(new FileReader(file)); + // Store this explicitly for logging. + addition.setPublicId(file.toString()); + layoutFiles.add(addition); + } catch (FileNotFoundException fileNotFoundException) { + mGeneralErrors.add("File not found " + + fileNotFoundException); + } + } + } + + return layoutFiles; + } + + /* + * Processes a list of files via an AccessibilityValidationContentHandler. + * The caller will only be notified of errors via logging. + */ + public void validateFiles() { + sLogger.info("Validating " + getLayoutFiles().size()); + XMLReader reader; + try { + reader = XMLReaderFactory.createXMLReader(); + } catch (SAXException saxExp) { + mGeneralErrors.add("Error " + saxExp); + return; + } + for (InputSource file : getLayoutFiles()) { + try { + AccessibilityValidationContentHandler contentHandler + = new AccessibilityValidationContentHandler( + file.getPublicId(), mAndroidSdkPath); + reader.setContentHandler(contentHandler); + reader.parse(file); + mTotalValidationErrors += contentHandler.getValidationErrors(); + } catch (IOException ioExp) { + mGeneralErrors.add("Error reading file " + ioExp); + } catch (SAXException saxExp) { + mGeneralErrors.add("Error " + saxExp); + } + } + } + + /** + * Returns the number of general errors (considered caught exceptions). + */ + public List getGeneralErrors() { + return mGeneralErrors; + } + + /** + * Sets the files to be tested. + */ + public void setLayoutFiles(List layoutFiles) { + this.mLayoutFiles = layoutFiles; + } + + /** + * Gets the files to be tested. + */ + public List getLayoutFiles() { + return mLayoutFiles; + } + + /** + * Gets the total number of test validation errors across all files. + */ + public int getTotalValidationErrors() { + return mTotalValidationErrors; + } +} -- cgit v1.2.3