/*
* Copyright (C) 2011 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.utils;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Map;
import java.util.Stack;
/**
* A handler for parsing simple HTML from Android WebView.
*/
public class WebContentHandler extends DefaultHandler {
/** Maps input type attribute to element description. */
private final Map<String, String> mInputTypeToDesc;
/** Maps ARIA role attribute to element description. */
private final Map<String, String> mAriaRoleToDesc;
/** Map tags to element description. */
private final Map<String, String> mTagToDesc;
/** A stack for storing post-order text generated by opening tags. */
private Stack<String> mPostorderTextStack;
/** Builder for a string to be spoken based on parsed HTML. */
private StringBuilder mOutputBuilder;
/**
* Initializes the handler with maps that provide descriptions for relevant
* features in HTML.
*
* @param htmlInputMap A mapping from input types to text descriptions.
* @param htmlRoleMap A mapping from ARIA roles to text descriptions.
* @param htmlTagMap A mapping from common tags to text descriptions.
*/
public WebContentHandler(Map<String, String> htmlInputMap, Map<String, String> htmlRoleMap,
Map<String, String> htmlTagMap) {
mInputTypeToDesc = htmlInputMap;
mAriaRoleToDesc = htmlRoleMap;
mTagToDesc = htmlTagMap;
}
@Override
public void startDocument() {
mOutputBuilder = new StringBuilder();
mPostorderTextStack = new Stack<>();
}
/**
* Depending on the type of element, generate text describing its conceptual
* value and role and add it to the output. The role text is spoken after
* any content, so it is added to the stack to wait for the closing tag.
*/
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) {
fixWhiteSpace();
final String ariaLabel = attributes.getValue("aria-label");
final String alt = attributes.getValue("alt");
final String title = attributes.getValue("title");
if (ariaLabel != null) {
mOutputBuilder.append(ariaLabel);
} else if (alt != null) {
mOutputBuilder.append(alt);
} else if (title != null) {
mOutputBuilder.append(title);
}
/*
* Add role text to the stack so it appears after the content. If there
* is no text we still need to push a blank string, since this will pop
* when this element ends.
*/
final String role = attributes.getValue("role");
final String roleName = mAriaRoleToDesc.get(role);
final String type = attributes.getValue("type");
final String tagInfo = mTagToDesc.get(name.toLowerCase());
if (roleName != null) {
mPostorderTextStack.push(roleName);
} else if (name.equalsIgnoreCase("input") && (type != null)) {
final String typeInfo = mInputTypeToDesc.get(type.toLowerCase());
if (typeInfo != null) {
mPostorderTextStack.push(typeInfo);
} else {
mPostorderTextStack.push("");
}
} else if (tagInfo != null) {
mPostorderTextStack.push(tagInfo);
} else {
mPostorderTextStack.push("");
}
/*
* The value should be spoken as long as the element is not a form
* element with a non-human-readable value.
*/
final String value = attributes.getValue("value");
if (value != null) {
String elementType = name;
if (name.equalsIgnoreCase("input") && (type != null)) {
elementType = type;
}
if (!elementType.equalsIgnoreCase("checkbox") && !elementType.equalsIgnoreCase("radio")) {
fixWhiteSpace();
mOutputBuilder.append(value);
}
}
}
/**
* Character data is passed directly to output.
*/
@Override
public void characters(char[] ch, int start, int length) {
mOutputBuilder.append(ch, start, length);
}
/**
* After the end of an element, get the post-order text from the stack and
* add it to the output.
*/
@Override
public void endElement(String uri, String localName, String name) {
final String postorderText = mPostorderTextStack.pop();
if (postorderText.length() > 0) {
fixWhiteSpace();
}
mOutputBuilder.append(postorderText);
}
/**
* Ensure the output string has a character of whitespace before adding
* another word.
*/
void fixWhiteSpace() {
final int index = mOutputBuilder.length() - 1;
if (index >= 0) {
final char lastCharacter = mOutputBuilder.charAt(index);
if (!Character.isWhitespace(lastCharacter)) {
mOutputBuilder.append(" ");
}
}
}
/**
* Get the processed string in mBuilder. Call this after parsing is done to
* get the finished output.
*
* @return A string with HTML tags converted to descriptions suitable for
* speaking.
*/
public String getOutput() {
return mOutputBuilder.toString();
}
}