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

http://eyes-free.googlecode.com/ · Java · 170 lines · 91 code · 18 blank · 61 comment · 31 complexity · 596b2e20b47a128100a7c521f336d74f MD5 · raw file

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