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

http://eyes-free.googlecode.com/ · Java · 307 lines · 187 code · 55 blank · 65 comment · 32 complexity · f7bd4115521ff780acfe5d54e0aae0fa MD5 · raw file

  1. /*
  2. * Copyright (C) 2009 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.google.android.marvin.talkback.formatter;
  17. import com.google.android.marvin.talkback.Formatter;
  18. import com.google.android.marvin.talkback.R;
  19. import com.google.android.marvin.talkback.TalkBackService;
  20. import com.google.android.marvin.talkback.Utils;
  21. import com.google.android.marvin.talkback.Utterance;
  22. import android.content.Context;
  23. import android.os.Bundle;
  24. import android.text.TextUtils;
  25. import android.view.accessibility.AccessibilityEvent;
  26. import java.util.HashMap;
  27. import java.util.Map;
  28. /**
  29. * This class contains custom formatters for presenting text edits.
  30. *
  31. * @author svetoslavganov@google.com (Svetoslav Ganov)
  32. */
  33. public final class TextFormatters {
  34. private static final Context CONTEXT = TalkBackService.asContext();
  35. private static final String SEPARATOR = " ";
  36. /**
  37. * This table will force the TTS to speak out the punctuation by mapping
  38. * punctuation to its spoken equivalent.
  39. */
  40. private static final HashMap<String, CharSequence> sPunctuationSpokenEquivalentsMap =
  41. new HashMap<String, CharSequence>();
  42. private TextFormatters() {
  43. // Not publicly instantiable.
  44. }
  45. /**
  46. * Formatter that returns an utterance to announce text addition.
  47. */
  48. public static final class AddedTextFormatter implements Formatter {
  49. @Override
  50. public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
  51. Bundle args) {
  52. final StringBuilder utteranceText = utterance.getText();
  53. if (event.isPassword()) {
  54. final String message = context.getString(R.string.value_password_character_typed);
  55. utteranceText.append(message);
  56. return true;
  57. }
  58. final CharSequence text = Utils.getEventAggregateText(context, event);
  59. final int begIndex = event.getFromIndex();
  60. final int endIndex = begIndex + event.getAddedCount();
  61. if ((begIndex < 0) || (endIndex > text.length())) {
  62. return false;
  63. }
  64. final CharSequence addedText = text.subSequence(begIndex, endIndex);
  65. utteranceText.append(formatForSpeech(addedText));
  66. return true;
  67. }
  68. }
  69. /**
  70. * Formatter that returns an utterance to announce text removing.
  71. */
  72. public static final class RemovedTextFormatter implements Formatter {
  73. @Override
  74. public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
  75. Bundle args) {
  76. final StringBuilder utteranceText = utterance.getText();
  77. if (event.isPassword()) {
  78. utteranceText.append(context.getString(R.string.value_text_removed));
  79. return true;
  80. }
  81. final CharSequence beforeText = event.getBeforeText();
  82. final int begIndex = event.getFromIndex();
  83. final int endIndex = begIndex + event.getRemovedCount();
  84. if ((begIndex < 0) || (endIndex > beforeText.length())) {
  85. return false;
  86. }
  87. final CharSequence removedText = beforeText.subSequence(begIndex, endIndex);
  88. utteranceText.append(formatForSpeech(removedText));
  89. utteranceText.append(SEPARATOR);
  90. utteranceText.append(context.getString(R.string.value_text_removed));
  91. return true;
  92. }
  93. }
  94. /**
  95. * Formatter that returns an utterance to announce text replacement.
  96. */
  97. public static final class ReplacedTextFormatter implements Formatter {
  98. @Override
  99. public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
  100. Bundle args) {
  101. final StringBuilder utteranceText = utterance.getText();
  102. if (event.isPassword()) {
  103. final int removed = event.getRemovedCount();
  104. final int added = event.getAddedCount();
  105. final String formattedText =
  106. context.getString(R.string.template_replaced_characters, removed, added);
  107. utteranceText.append(formattedText);
  108. return true;
  109. }
  110. final String text = Utils.getEventText(context, event).toString();
  111. final String beforeText = event.getBeforeText().toString();
  112. if (isTypedCharacter(text, beforeText)) {
  113. final CharSequence appendText = formatCharacterChange(text);
  114. if (TextUtils.isEmpty(appendText)) {
  115. return false;
  116. }
  117. utteranceText.append(appendText);
  118. } else if (isRemovedCharacter(text, beforeText)) {
  119. final CharSequence appendText = formatCharacterChange(beforeText);
  120. if (TextUtils.isEmpty(appendText)) {
  121. return false;
  122. }
  123. utteranceText.append(appendText);
  124. utteranceText.append(SEPARATOR);
  125. utteranceText.append(context.getString(R.string.value_text_removed));
  126. } else {
  127. final CharSequence appendText = formatTextChange(event, context, text, beforeText);
  128. if (TextUtils.isEmpty(appendText)) {
  129. return false;
  130. }
  131. utteranceText.append(appendText);
  132. }
  133. return true;
  134. }
  135. private CharSequence formatTextChange(AccessibilityEvent event, Context context,
  136. String text, String beforeText) {
  137. final int beforeBegIndex = event.getFromIndex();
  138. final int beforeEndIndex = beforeBegIndex + event.getRemovedCount();
  139. if (beforeBegIndex < 0 || beforeEndIndex > beforeText.length()) {
  140. return null;
  141. }
  142. final CharSequence removedText =
  143. formatForSpeech(beforeText.subSequence(beforeBegIndex, beforeEndIndex));
  144. final int addedBegIndex = event.getFromIndex();
  145. final int addedEndIndex = addedBegIndex + event.getAddedCount();
  146. if (addedBegIndex < 0 || addedEndIndex > text.length()) {
  147. return null;
  148. }
  149. final CharSequence addedText =
  150. formatForSpeech(text.subSequence(addedBegIndex, addedEndIndex));
  151. final CharSequence formattedText =
  152. context.getString(R.string.template_text_replaced, removedText, addedText);
  153. return formattedText;
  154. }
  155. private CharSequence formatCharacterChange(String text) {
  156. // This happens if the application replaces the entire text
  157. // while the user is typing. This logic leads to missing
  158. // the very rare case of the user replacing text with the
  159. // same text plus a single character but handles the much
  160. // more frequent case mentioned above.
  161. final int begIndex = text.length() - 1;
  162. final int endIndex = begIndex + 1;
  163. if ((begIndex < 0) || (endIndex > text.length())) {
  164. return null;
  165. }
  166. final CharSequence addedText = text.subSequence(begIndex, endIndex);
  167. return formatForSpeech(addedText);
  168. }
  169. /**
  170. * Returns if a character is added to the event <code>text</code> given
  171. * the event <code>beforeText</code>.
  172. */
  173. private boolean isTypedCharacter(String text, String beforeText) {
  174. if (text.length() != (beforeText.length() + 1)) {
  175. return false;
  176. }
  177. return text.startsWith(beforeText);
  178. }
  179. /**
  180. * Returns if a character is removed from the event <code>text</code>
  181. * given the event <code>beforeText</code>.
  182. */
  183. private boolean isRemovedCharacter(String text, String beforeText) {
  184. if ((text.length() + 1) != beforeText.length()) {
  185. return false;
  186. }
  187. return beforeText.startsWith(text);
  188. }
  189. }
  190. /**
  191. * Formatter that returns an utterance to announce text selection.
  192. */
  193. public static final class SelectedTextFormatter implements Formatter {
  194. @Override
  195. public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
  196. Bundle args) {
  197. final CharSequence text = Utils.getEventText(context, event);
  198. final int begIndex = event.getFromIndex();
  199. final int endIndex = event.getToIndex();
  200. if ((begIndex < 0) || (endIndex > text.length()) || (begIndex >= endIndex)) {
  201. return false;
  202. }
  203. final CharSequence selectedText = text.subSequence(begIndex, endIndex);
  204. utterance.getText().append(formatForSpeech(selectedText));
  205. return true;
  206. }
  207. }
  208. /**
  209. * Gets the spoken equivalent of a character. Passing an argument longer
  210. * that one return the argument itself as the spoken equivalent. </p> Note:
  211. * The argument is a {@link CharSequence} for efficiency to avoid multiple
  212. * string creation.
  213. *
  214. * @param character The character to transform.
  215. * @return The spoken equivalent.
  216. */
  217. private static CharSequence formatForSpeech(CharSequence character) {
  218. if (character.length() != 1) {
  219. return character;
  220. }
  221. final CharSequence mapping = getPunctuationSpokenEquivalentMap().get(character);
  222. if (mapping != null) {
  223. return mapping;
  224. }
  225. return character;
  226. }
  227. /**
  228. * Gets the spoken equivalent map. If the map is not initialized it is first
  229. * create and populated.
  230. *
  231. * @return The spoken equivalent map.
  232. */
  233. private static Map<String, CharSequence> getPunctuationSpokenEquivalentMap() {
  234. if (sPunctuationSpokenEquivalentsMap.isEmpty()) {
  235. loadMapping("?", R.string.punctuation_questionmark);
  236. loadMapping(" ", R.string.punctuation_space);
  237. loadMapping(",", R.string.punctuation_comma);
  238. loadMapping(".", R.string.punctuation_dot);
  239. loadMapping("!", R.string.punctuation_exclamation);
  240. loadMapping("(", R.string.punctuation_open_paren);
  241. loadMapping(")", R.string.punctuation_close_paren);
  242. loadMapping("\"", R.string.punctuation_double_quote);
  243. loadMapping(";", R.string.punctuation_semicolon);
  244. loadMapping(":", R.string.punctuation_colon);
  245. }
  246. return sPunctuationSpokenEquivalentsMap;
  247. }
  248. private static void loadMapping(String text, int resId) {
  249. final CharSequence spoken = CONTEXT.getString(resId);
  250. sPunctuationSpokenEquivalentsMap.put(text, spoken);
  251. }
  252. }