PageRenderTime 33ms CodeModel.GetById 10ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/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
 17package com.google.android.marvin.talkback.formatter;
 18
 19import com.google.android.marvin.talkback.Formatter;
 20import com.google.android.marvin.talkback.R;
 21import com.google.android.marvin.talkback.TalkBackService;
 22import com.google.android.marvin.talkback.Utils;
 23import com.google.android.marvin.talkback.Utterance;
 24
 25import android.content.Context;
 26import android.os.Bundle;
 27import android.text.TextUtils;
 28import android.view.accessibility.AccessibilityEvent;
 29
 30import java.util.HashMap;
 31import java.util.Map;
 32
 33/**
 34 * This class contains custom formatters for presenting text edits.
 35 * 
 36 * @author svetoslavganov@google.com (Svetoslav Ganov)
 37 */
 38public final class TextFormatters {
 39    private static final Context CONTEXT = TalkBackService.asContext();
 40    private static final String SEPARATOR = " ";
 41
 42    /**
 43     * This table will force the TTS to speak out the punctuation by mapping
 44     * punctuation to its spoken equivalent.
 45     */
 46    private static final HashMap<String, CharSequence> sPunctuationSpokenEquivalentsMap =
 47            new HashMap<String, CharSequence>();
 48
 49    private TextFormatters() {
 50        // Not publicly instantiable.
 51    }
 52
 53    /**
 54     * Formatter that returns an utterance to announce text addition.
 55     */
 56    public static final class AddedTextFormatter implements Formatter {
 57        @Override
 58        public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
 59                Bundle args) {
 60            final StringBuilder utteranceText = utterance.getText();
 61
 62            if (event.isPassword()) {
 63                final String message = context.getString(R.string.value_password_character_typed);
 64                utteranceText.append(message);
 65                return true;
 66            }
 67
 68            final CharSequence text = Utils.getEventAggregateText(context, event);
 69            final int begIndex = event.getFromIndex();
 70            final int endIndex = begIndex + event.getAddedCount();
 71
 72            if ((begIndex < 0) || (endIndex > text.length())) {
 73                return false;
 74            }
 75
 76            final CharSequence addedText = text.subSequence(begIndex, endIndex);
 77            utteranceText.append(formatForSpeech(addedText));
 78
 79            return true;
 80        }
 81    }
 82
 83    /**
 84     * Formatter that returns an utterance to announce text removing.
 85     */
 86    public static final class RemovedTextFormatter implements Formatter {
 87        @Override
 88        public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
 89                Bundle args) {
 90            final StringBuilder utteranceText = utterance.getText();
 91
 92            if (event.isPassword()) {
 93                utteranceText.append(context.getString(R.string.value_text_removed));
 94                return true;
 95            }
 96
 97            final CharSequence beforeText = event.getBeforeText();
 98            final int begIndex = event.getFromIndex();
 99            final int endIndex = begIndex + event.getRemovedCount();
100
101            if ((begIndex < 0) || (endIndex > beforeText.length())) {
102                return false;
103            }
104
105            final CharSequence removedText = beforeText.subSequence(begIndex, endIndex);
106            utteranceText.append(formatForSpeech(removedText));
107            utteranceText.append(SEPARATOR);
108            utteranceText.append(context.getString(R.string.value_text_removed));
109
110            return true;
111        }
112    }
113
114    /**
115     * Formatter that returns an utterance to announce text replacement.
116     */
117    public static final class ReplacedTextFormatter implements Formatter {
118        @Override
119        public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
120                Bundle args) {
121            final StringBuilder utteranceText = utterance.getText();
122
123            if (event.isPassword()) {
124                final int removed = event.getRemovedCount();
125                final int added = event.getAddedCount();
126                final String formattedText =
127                        context.getString(R.string.template_replaced_characters, removed, added);
128                utteranceText.append(formattedText);
129                return true;
130            }
131
132            final String text = Utils.getEventText(context, event).toString();
133            final String beforeText = event.getBeforeText().toString();
134
135            if (isTypedCharacter(text, beforeText)) {
136                final CharSequence appendText = formatCharacterChange(text);
137
138                if (TextUtils.isEmpty(appendText)) {
139                    return false;
140                }
141
142                utteranceText.append(appendText);
143            } else if (isRemovedCharacter(text, beforeText)) {
144                final CharSequence appendText = formatCharacterChange(beforeText);
145
146                if (TextUtils.isEmpty(appendText)) {
147                    return false;
148                }
149
150                utteranceText.append(appendText);
151                utteranceText.append(SEPARATOR);
152                utteranceText.append(context.getString(R.string.value_text_removed));
153            } else {
154                final CharSequence appendText = formatTextChange(event, context, text, beforeText);
155
156                if (TextUtils.isEmpty(appendText)) {
157                    return false;
158                }
159
160                utteranceText.append(appendText);
161            }
162
163            return true;
164        }
165
166        private CharSequence formatTextChange(AccessibilityEvent event, Context context,
167                String text, String beforeText) {
168            final int beforeBegIndex = event.getFromIndex();
169            final int beforeEndIndex = beforeBegIndex + event.getRemovedCount();
170
171            if (beforeBegIndex < 0 || beforeEndIndex > beforeText.length()) {
172                return null;
173            }
174
175            final CharSequence removedText =
176                    formatForSpeech(beforeText.subSequence(beforeBegIndex, beforeEndIndex));
177
178            final int addedBegIndex = event.getFromIndex();
179            final int addedEndIndex = addedBegIndex + event.getAddedCount();
180
181            if (addedBegIndex < 0 || addedEndIndex > text.length()) {
182                return null;
183            }
184
185            final CharSequence addedText =
186                    formatForSpeech(text.subSequence(addedBegIndex, addedEndIndex));
187
188            final CharSequence formattedText =
189                    context.getString(R.string.template_text_replaced, removedText, addedText);
190            return formattedText;
191        }
192
193        private CharSequence formatCharacterChange(String text) {
194            // This happens if the application replaces the entire text
195            // while the user is typing. This logic leads to missing
196            // the very rare case of the user replacing text with the
197            // same text plus a single character but handles the much
198            // more frequent case mentioned above.
199            final int begIndex = text.length() - 1;
200            final int endIndex = begIndex + 1;
201
202            if ((begIndex < 0) || (endIndex > text.length())) {
203                return null;
204            }
205
206            final CharSequence addedText = text.subSequence(begIndex, endIndex);
207
208            return formatForSpeech(addedText);
209        }
210
211        /**
212         * Returns if a character is added to the event <code>text</code> given
213         * the event <code>beforeText</code>.
214         */
215        private boolean isTypedCharacter(String text, String beforeText) {
216            if (text.length() != (beforeText.length() + 1)) {
217                return false;
218            }
219
220            return text.startsWith(beforeText);
221        }
222
223        /**
224         * Returns if a character is removed from the event <code>text</code>
225         * given the event <code>beforeText</code>.
226         */
227        private boolean isRemovedCharacter(String text, String beforeText) {
228            if ((text.length() + 1) != beforeText.length()) {
229                return false;
230            }
231
232            return beforeText.startsWith(text);
233        }
234    }
235
236    /**
237     * Formatter that returns an utterance to announce text selection.
238     */
239    public static final class SelectedTextFormatter implements Formatter {
240        @Override
241        public boolean format(AccessibilityEvent event, Context context, Utterance utterance,
242                Bundle args) {
243            final CharSequence text = Utils.getEventText(context, event);
244            final int begIndex = event.getFromIndex();
245            final int endIndex = event.getToIndex();
246
247            if ((begIndex < 0) || (endIndex > text.length()) || (begIndex >= endIndex)) {
248                return false;
249            }
250
251            final CharSequence selectedText = text.subSequence(begIndex, endIndex);
252            utterance.getText().append(formatForSpeech(selectedText));
253            
254            return true;
255        }
256    }
257
258    /**
259     * Gets the spoken equivalent of a character. Passing an argument longer
260     * that one return the argument itself as the spoken equivalent. </p> Note:
261     * The argument is a {@link CharSequence} for efficiency to avoid multiple
262     * string creation.
263     * 
264     * @param character The character to transform.
265     * @return The spoken equivalent.
266     */
267    private static CharSequence formatForSpeech(CharSequence character) {
268        if (character.length() != 1) {
269            return character;
270        }
271
272        final CharSequence mapping = getPunctuationSpokenEquivalentMap().get(character);
273        if (mapping != null) {
274            return mapping;
275        }
276
277        return character;
278    }
279
280    /**
281     * Gets the spoken equivalent map. If the map is not initialized it is first
282     * create and populated.
283     * 
284     * @return The spoken equivalent map.
285     */
286    private static Map<String, CharSequence> getPunctuationSpokenEquivalentMap() {
287        if (sPunctuationSpokenEquivalentsMap.isEmpty()) {
288            loadMapping("?", R.string.punctuation_questionmark);
289            loadMapping(" ", R.string.punctuation_space);
290            loadMapping(",", R.string.punctuation_comma);
291            loadMapping(".", R.string.punctuation_dot);
292            loadMapping("!", R.string.punctuation_exclamation);
293            loadMapping("(", R.string.punctuation_open_paren);
294            loadMapping(")", R.string.punctuation_close_paren);
295            loadMapping("\"", R.string.punctuation_double_quote);
296            loadMapping(";", R.string.punctuation_semicolon);
297            loadMapping(":", R.string.punctuation_colon);
298        }
299
300        return sPunctuationSpokenEquivalentsMap;
301    }
302
303    private static void loadMapping(String text, int resId) {
304        final CharSequence spoken = CONTEXT.getString(resId);
305        sPunctuationSpokenEquivalentsMap.put(text, spoken);
306    }
307}