PageRenderTime 41ms CodeModel.GetById 7ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 1ms

/TalkBack/src/com/google/android/marvin/talkback/formatter/WebContentHandler.java

http://eyes-free.googlecode.com/
Java | 179 lines | 90 code | 28 blank | 61 comment | 31 complexity | fde9f6f76999310c2339375596219468 MD5 | raw file
  1// Copyright 2010 Google Inc. All Rights Reserved.
  2
  3package com.google.android.marvin.talkback.formatter;
  4
  5import org.xml.sax.Attributes;
  6import org.xml.sax.helpers.DefaultHandler;
  7
  8import java.util.Map;
  9import java.util.Stack;
 10
 11/**
 12 * A handler for parsing simple HTML from Android WebView.
 13 *
 14 * @author credo@google.com (Tim Credo)
 15 */
 16public class WebContentHandler extends DefaultHandler {
 17
 18    /**
 19     * Maps input type attribute to element description.
 20     */
 21    private final Map<String, String> mInputTypeToDesc;
 22
 23    /**
 24     * Maps ARIA role attribute to element description.
 25     */
 26    private final Map<String, String> mAriaRoleToDesc;
 27
 28    /**
 29     * Map tags to element description.
 30     */
 31    private final Map<String, String> mTagToDesc;
 32
 33    /**
 34     * A stack for storing post-order text generated by opening tags.
 35     */
 36    private Stack<String> mPostorderTextStack;
 37
 38    /**
 39     * Builder for a string to be spoken based on parsed HTML.
 40     */
 41    private StringBuilder mOutputBuilder;
 42
 43    /**
 44     * Initializes the handler with maps that provide descriptions for relevant
 45     * features in HTML.
 46     *
 47     * @param htmlInputMap A mapping from input types to text descriptions.
 48     * @param htmlRoleMap A mapping from ARIA roles to text descriptions.
 49     * @param htmlTagMap A mapping from common tags to text descriptions.
 50     */
 51    public WebContentHandler(Map<String, String> htmlInputMap, Map<String, String> htmlRoleMap,
 52            Map<String, String> htmlTagMap) {
 53        mInputTypeToDesc = htmlInputMap;
 54        mAriaRoleToDesc = htmlRoleMap;
 55        mTagToDesc = htmlTagMap;
 56    }
 57
 58    @Override
 59    public void startDocument() {
 60        mOutputBuilder = new StringBuilder();
 61        mPostorderTextStack = new Stack<String>();
 62    }
 63
 64    /**
 65     * Depending on the type of element, generate text describing its conceptual
 66     * value and role and add it to the output. The role text is spoken after
 67     * any content, so it is added to the stack to wait for the closing tag.
 68     */
 69    @Override
 70    public void startElement(String uri, String localName, String name, Attributes attributes) {
 71        fixWhiteSpace();
 72        final String ariaLabel = attributes.getValue("aria-label");
 73        final String alt = attributes.getValue("alt");
 74        final String title = attributes.getValue("title");
 75
 76        if (ariaLabel != null) {
 77            mOutputBuilder.append(ariaLabel);
 78        } else if (alt != null) {
 79            mOutputBuilder.append(alt);
 80        } else if (title != null) {
 81            mOutputBuilder.append(title);
 82        }
 83
 84        /*
 85         * Add role text to the stack so it appears after the content. If there
 86         * is no text we still need to push a blank string, since this will pop
 87         * when this element ends.
 88         */
 89        final String role = attributes.getValue("role");
 90        final String roleName = mAriaRoleToDesc.get(role);
 91        final String type = attributes.getValue("type");
 92        final String tagInfo = mTagToDesc.get(name.toLowerCase());
 93
 94        if (roleName != null) {
 95            mPostorderTextStack.push(roleName);
 96        } else if (name.equalsIgnoreCase("input") && (type != null)) {
 97            final String typeInfo = mInputTypeToDesc.get(type.toLowerCase());
 98
 99            if (typeInfo != null) {
100                mPostorderTextStack.push(typeInfo);
101            } else {
102                mPostorderTextStack.push("");
103            }
104        } else if (tagInfo != null) {
105            mPostorderTextStack.push(tagInfo);
106        } else {
107            mPostorderTextStack.push("");
108        }
109
110        /*
111         * The value should be spoken as long as the element is not a form
112         * element with a non-human-readable value.
113         */
114        final String value = attributes.getValue("value");
115
116        if (value != null) {
117            String elementType = name;
118
119            if (name.equalsIgnoreCase("input") && (type != null)) {
120                elementType = type;
121            }
122
123            if (!elementType.equalsIgnoreCase("checkbox") && !elementType.equalsIgnoreCase("radio")) {
124                fixWhiteSpace();
125                mOutputBuilder.append(value);
126            }
127        }
128    }
129
130    /**
131     * Character data is passed directly to output.
132     */
133    @Override
134    public void characters(char[] ch, int start, int length) {
135        mOutputBuilder.append(ch, start, length);
136    }
137
138    /**
139     * After the end of an element, get the post-order text from the stack and
140     * add it to the output.
141     */
142    @Override
143    public void endElement(String uri, String localName, String name) {
144        final String postorderText = mPostorderTextStack.pop();
145
146        if (postorderText.length() > 0) {
147            fixWhiteSpace();
148        }
149
150        mOutputBuilder.append(postorderText);
151    }
152
153    /**
154     * Ensure the output string has a character of whitespace before adding
155     * another word.
156     */
157    public void fixWhiteSpace() {
158        final int index = mOutputBuilder.length() - 1;
159
160        if (index >= 0) {
161            final char lastCharacter = mOutputBuilder.charAt(index);
162
163            if (!Character.isWhitespace(lastCharacter)) {
164                mOutputBuilder.append(" ");
165            }
166        }
167    }
168
169    /**
170     * Get the processed string in mBuilder. Call this after parsing is done to
171     * get the finished output.
172     *
173     * @return A string with HTML tags converted to descriptions suitable for
174     *         speaking.
175     */
176    public String getOutput() {
177        return mOutputBuilder.toString();
178    }
179}