summaryrefslogtreecommitdiff
path: root/AccessCheck/src/com/android/accessibility/AccessibilityValidationContentHandler.java
blob: 77d67f023ba8de2b0db0a60bedf3ce20b1303e4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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:
 * <p>
 * If the Element tag is ImageView (or a subclass of ImageView), then the tag
 * must contain a contentDescription attribute.
 * <p>
 * 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<String> mExclusionList = new HashSet<String>();

    /** 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<String> sExpectedAttributes =
            new HashSet<String>();

    /** 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");
    }
}