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