• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.voice;
18 
19 import com.android.inputmethod.latin.EditingUtil;
20 import com.android.inputmethod.latin.R;
21 
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.Message;
29 import android.os.Parcelable;
30 import android.speech.RecognitionListener;
31 import android.speech.SpeechRecognizer;
32 import android.speech.RecognizerIntent;
33 import android.util.Log;
34 import android.view.inputmethod.InputConnection;
35 import android.view.View;
36 import android.view.View.OnClickListener;
37 
38 import java.io.ByteArrayOutputStream;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 
46 /**
47  * Speech recognition input, including both user interface and a background
48  * process to stream audio to the network recognizer. This class supplies a
49  * View (getView()), which it updates as recognition occurs. The user of this
50  * class is responsible for making the view visible to the user, as well as
51  * handling various events returned through UiListener.
52  */
53 public class VoiceInput implements OnClickListener {
54     private static final String TAG = "VoiceInput";
55     private static final String EXTRA_RECOGNITION_CONTEXT =
56             "android.speech.extras.RECOGNITION_CONTEXT";
57     private static final String EXTRA_CALLING_PACKAGE = "calling_package";
58     private static final String EXTRA_ALTERNATES = "android.speech.extra.ALTERNATES";
59     private static final int MAX_ALT_LIST_LENGTH = 6;
60 
61     private static final String DEFAULT_RECOMMENDED_PACKAGES =
62             "com.android.mms " +
63             "com.google.android.gm " +
64             "com.google.android.talk " +
65             "com.google.android.apps.googlevoice " +
66             "com.android.email " +
67             "com.android.browser ";
68 
69     // WARNING! Before enabling this, fix the problem with calling getExtractedText() in
70     // landscape view. It causes Extracted text updates to be rejected due to a token mismatch
71     public static boolean ENABLE_WORD_CORRECTIONS = true;
72 
73     // Dummy word suggestion which means "delete current word"
74     public static final String DELETE_SYMBOL = " \u00D7 ";  // times symbol
75 
76     private Whitelist mRecommendedList;
77     private Whitelist mBlacklist;
78 
79     private VoiceInputLogger mLogger;
80 
81     // Names of a few extras defined in VoiceSearch's RecognitionController
82     // Note, the version of voicesearch that shipped in Froyo returns the raw
83     // RecognitionClientAlternates protocol buffer under the key "alternates",
84     // so a VS market update must be installed on Froyo devices in order to see
85     // alternatives.
86     private static final String ALTERNATES_BUNDLE = "alternates_bundle";
87 
88     //  This is copied from the VoiceSearch app.
89     private static final class AlternatesBundleKeys {
90         public static final String ALTERNATES = "alternates";
91         public static final String CONFIDENCE = "confidence";
92         public static final String LENGTH = "length";
93         public static final String MAX_SPAN_LENGTH = "max_span_length";
94         public static final String SPANS = "spans";
95         public static final String SPAN_KEY_DELIMITER = ":";
96         public static final String START = "start";
97         public static final String TEXT = "text";
98     }
99 
100     // Names of a few intent extras defined in VoiceSearch's RecognitionService.
101     // These let us tweak the endpointer parameters.
102     private static final String EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS =
103             "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS";
104     private static final String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS =
105             "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS";
106     private static final String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS =
107             "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS";
108 
109     // The usual endpointer default value for input complete silence length is 0.5 seconds,
110     // but that's used for things like voice search. For dictation-like voice input like this,
111     // we go with a more liberal value of 1 second. This value will only be used if a value
112     // is not provided from Gservices.
113     private static final String INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS = "1000";
114 
115     // Used to record part of that state for logging purposes.
116     public static final int DEFAULT = 0;
117     public static final int LISTENING = 1;
118     public static final int WORKING = 2;
119     public static final int ERROR = 3;
120 
121     private int mAfterVoiceInputDeleteCount = 0;
122     private int mAfterVoiceInputInsertCount = 0;
123     private int mAfterVoiceInputInsertPunctuationCount = 0;
124     private int mAfterVoiceInputCursorPos = 0;
125     private int mAfterVoiceInputSelectionSpan = 0;
126 
127     private int mState = DEFAULT;
128 
129     private final static int MSG_CLOSE_ERROR_DIALOG = 1;
130 
131     private final Handler mHandler = new Handler() {
132         @Override
133         public void handleMessage(Message msg) {
134             if (msg.what == MSG_CLOSE_ERROR_DIALOG) {
135                 mState = DEFAULT;
136                 mRecognitionView.finish();
137                 mUiListener.onCancelVoice();
138             }
139         }
140     };
141 
142     /**
143      * Events relating to the recognition UI. You must implement these.
144      */
145     public interface UiListener {
146 
147         /**
148          * @param recognitionResults a set of transcripts for what the user
149          *   spoke, sorted by likelihood.
150          */
onVoiceResults( List<String> recognitionResults, Map<String, List<CharSequence>> alternatives)151         public void onVoiceResults(
152             List<String> recognitionResults,
153             Map<String, List<CharSequence>> alternatives);
154 
155         /**
156          * Called when the user cancels speech recognition.
157          */
onCancelVoice()158         public void onCancelVoice();
159     }
160 
161     private SpeechRecognizer mSpeechRecognizer;
162     private RecognitionListener mRecognitionListener;
163     private RecognitionView mRecognitionView;
164     private UiListener mUiListener;
165     private Context mContext;
166 
167     /**
168      * @param context the service or activity in which we're running.
169      * @param uiHandler object to receive events from VoiceInput.
170      */
VoiceInput(Context context, UiListener uiHandler)171     public VoiceInput(Context context, UiListener uiHandler) {
172         mLogger = VoiceInputLogger.getLogger(context);
173         mRecognitionListener = new ImeRecognitionListener();
174         mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
175         mSpeechRecognizer.setRecognitionListener(mRecognitionListener);
176         mUiListener = uiHandler;
177         mContext = context;
178         newView();
179 
180         String recommendedPackages = SettingsUtil.getSettingsString(
181                 context.getContentResolver(),
182                 SettingsUtil.LATIN_IME_VOICE_INPUT_RECOMMENDED_PACKAGES,
183                 DEFAULT_RECOMMENDED_PACKAGES);
184 
185         mRecommendedList = new Whitelist();
186         for (String recommendedPackage : recommendedPackages.split("\\s+")) {
187             mRecommendedList.addApp(recommendedPackage);
188         }
189 
190         mBlacklist = new Whitelist();
191         mBlacklist.addApp("com.android.setupwizard");
192     }
193 
setCursorPos(int pos)194     public void setCursorPos(int pos) {
195         mAfterVoiceInputCursorPos = pos;
196     }
197 
getCursorPos()198     public int getCursorPos() {
199         return mAfterVoiceInputCursorPos;
200     }
201 
setSelectionSpan(int span)202     public void setSelectionSpan(int span) {
203         mAfterVoiceInputSelectionSpan = span;
204     }
205 
getSelectionSpan()206     public int getSelectionSpan() {
207         return mAfterVoiceInputSelectionSpan;
208     }
209 
incrementTextModificationDeleteCount(int count)210     public void incrementTextModificationDeleteCount(int count){
211         mAfterVoiceInputDeleteCount += count;
212         // Send up intents for other text modification types
213         if (mAfterVoiceInputInsertCount > 0) {
214             logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
215             mAfterVoiceInputInsertCount = 0;
216         }
217         if (mAfterVoiceInputInsertPunctuationCount > 0) {
218             logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
219             mAfterVoiceInputInsertPunctuationCount = 0;
220         }
221 
222     }
223 
incrementTextModificationInsertCount(int count)224     public void incrementTextModificationInsertCount(int count){
225         mAfterVoiceInputInsertCount += count;
226         if (mAfterVoiceInputSelectionSpan > 0) {
227             // If text was highlighted before inserting the char, count this as
228             // a delete.
229             mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
230         }
231         // Send up intents for other text modification types
232         if (mAfterVoiceInputDeleteCount > 0) {
233             logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
234             mAfterVoiceInputDeleteCount = 0;
235         }
236         if (mAfterVoiceInputInsertPunctuationCount > 0) {
237             logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
238             mAfterVoiceInputInsertPunctuationCount = 0;
239         }
240     }
241 
incrementTextModificationInsertPunctuationCount(int count)242     public void incrementTextModificationInsertPunctuationCount(int count){
243         mAfterVoiceInputInsertPunctuationCount += 1;
244         if (mAfterVoiceInputSelectionSpan > 0) {
245             // If text was highlighted before inserting the char, count this as
246             // a delete.
247             mAfterVoiceInputDeleteCount += mAfterVoiceInputSelectionSpan;
248         }
249         // Send up intents for aggregated non-punctuation insertions
250         if (mAfterVoiceInputDeleteCount > 0) {
251             logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
252             mAfterVoiceInputDeleteCount = 0;
253         }
254         if (mAfterVoiceInputInsertCount > 0) {
255             logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
256             mAfterVoiceInputInsertCount = 0;
257         }
258     }
259 
flushAllTextModificationCounters()260     public void flushAllTextModificationCounters() {
261         if (mAfterVoiceInputInsertCount > 0) {
262             logTextModifiedByTypingInsertion(mAfterVoiceInputInsertCount);
263             mAfterVoiceInputInsertCount = 0;
264         }
265         if (mAfterVoiceInputDeleteCount > 0) {
266             logTextModifiedByTypingDeletion(mAfterVoiceInputDeleteCount);
267             mAfterVoiceInputDeleteCount = 0;
268         }
269         if (mAfterVoiceInputInsertPunctuationCount > 0) {
270             logTextModifiedByTypingInsertionPunctuation(mAfterVoiceInputInsertPunctuationCount);
271             mAfterVoiceInputInsertPunctuationCount = 0;
272         }
273     }
274 
275     /**
276      * The configuration of the IME changed and may have caused the views to be layed out
277      * again. Restore the state of the recognition view.
278      */
onConfigurationChanged()279     public void onConfigurationChanged() {
280         mRecognitionView.restoreState();
281     }
282 
283     /**
284      * @return true if field is blacklisted for voice
285      */
isBlacklistedField(FieldContext context)286     public boolean isBlacklistedField(FieldContext context) {
287         return mBlacklist.matches(context);
288     }
289 
290     /**
291      * Used to decide whether to show voice input hints for this field, etc.
292      *
293      * @return true if field is recommended for voice
294      */
isRecommendedField(FieldContext context)295     public boolean isRecommendedField(FieldContext context) {
296         return mRecommendedList.matches(context);
297     }
298 
299     /**
300      * Start listening for speech from the user. This will grab the microphone
301      * and start updating the view provided by getView(). It is the caller's
302      * responsibility to ensure that the view is visible to the user at this stage.
303      *
304      * @param context the same FieldContext supplied to voiceIsEnabled()
305      * @param swipe whether this voice input was started by swipe, for logging purposes
306      */
startListening(FieldContext context, boolean swipe)307     public void startListening(FieldContext context, boolean swipe) {
308         mState = DEFAULT;
309 
310         Locale locale = Locale.getDefault();
311         String localeString = locale.getLanguage() + "-" + locale.getCountry();
312 
313         mLogger.start(localeString, swipe);
314 
315         mState = LISTENING;
316 
317         mRecognitionView.showInitializing();
318         startListeningAfterInitialization(context);
319     }
320 
321     /**
322      * Called only when the recognition manager's initialization completed
323      *
324      * @param context context with which {@link #startListening(FieldContext, boolean)} was executed
325      */
startListeningAfterInitialization(FieldContext context)326     private void startListeningAfterInitialization(FieldContext context) {
327         Intent intent = makeIntent();
328         intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, "");
329         intent.putExtra(EXTRA_RECOGNITION_CONTEXT, context.getBundle());
330         intent.putExtra(EXTRA_CALLING_PACKAGE, "VoiceIME");
331         intent.putExtra(EXTRA_ALTERNATES, true);
332         intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS,
333                 SettingsUtil.getSettingsInt(
334                         mContext.getContentResolver(),
335                         SettingsUtil.LATIN_IME_MAX_VOICE_RESULTS,
336                         1));
337         // Get endpointer params from Gservices.
338         // TODO: Consider caching these values for improved performance on slower devices.
339         final ContentResolver cr = mContext.getContentResolver();
340         putEndpointerExtra(
341                 cr,
342                 intent,
343                 SettingsUtil.LATIN_IME_SPEECH_MINIMUM_LENGTH_MILLIS,
344                 EXTRA_SPEECH_MINIMUM_LENGTH_MILLIS,
345                 null  /* rely on endpointer default */);
346         putEndpointerExtra(
347                 cr,
348                 intent,
349                 SettingsUtil.LATIN_IME_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
350                 EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS,
351                 INPUT_COMPLETE_SILENCE_LENGTH_DEFAULT_VALUE_MILLIS
352                 /* our default value is different from the endpointer's */);
353         putEndpointerExtra(
354                 cr,
355                 intent,
356                 SettingsUtil.
357                         LATIN_IME_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
358                 EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS,
359                 null  /* rely on endpointer default */);
360 
361         mSpeechRecognizer.startListening(intent);
362     }
363 
364     /**
365      * Gets the value of the provided Gservices key, attempts to parse it into a long,
366      * and if successful, puts the long value as an extra in the provided intent.
367      */
putEndpointerExtra(ContentResolver cr, Intent i, String gservicesKey, String intentExtraKey, String defaultValue)368     private void putEndpointerExtra(ContentResolver cr, Intent i,
369             String gservicesKey, String intentExtraKey, String defaultValue) {
370         long l = -1;
371         String s = SettingsUtil.getSettingsString(cr, gservicesKey, defaultValue);
372         if (s != null) {
373             try {
374                 l = Long.valueOf(s);
375             } catch (NumberFormatException e) {
376                 Log.e(TAG, "could not parse value for " + gservicesKey + ": " + s);
377             }
378         }
379 
380         if (l != -1) i.putExtra(intentExtraKey, l);
381     }
382 
destroy()383     public void destroy() {
384         mSpeechRecognizer.destroy();
385     }
386 
387     /**
388      * Creates a new instance of the view that is returned by {@link #getView()}
389      * Clients should use this when a previously returned view is stuck in a
390      * layout that is being thrown away and a new one is need to show to the
391      * user.
392      */
newView()393     public void newView() {
394         mRecognitionView = new RecognitionView(mContext, this);
395     }
396 
397     /**
398      * @return a view that shows the recognition flow--e.g., "Speak now" and
399      * "working" dialogs.
400      */
getView()401     public View getView() {
402         return mRecognitionView.getView();
403     }
404 
405     /**
406      * Handle the cancel button.
407      */
onClick(View view)408     public void onClick(View view) {
409         switch(view.getId()) {
410             case R.id.button:
411                 cancel();
412                 break;
413         }
414     }
415 
logTextModifiedByTypingInsertion(int length)416     public void logTextModifiedByTypingInsertion(int length) {
417         mLogger.textModifiedByTypingInsertion(length);
418     }
419 
logTextModifiedByTypingInsertionPunctuation(int length)420     public void logTextModifiedByTypingInsertionPunctuation(int length) {
421         mLogger.textModifiedByTypingInsertionPunctuation(length);
422     }
423 
logTextModifiedByTypingDeletion(int length)424     public void logTextModifiedByTypingDeletion(int length) {
425         mLogger.textModifiedByTypingDeletion(length);
426     }
427 
logTextModifiedByChooseSuggestion(String suggestion, int index, String wordSeparators, InputConnection ic)428     public void logTextModifiedByChooseSuggestion(String suggestion, int index,
429                                                   String wordSeparators, InputConnection ic) {
430         EditingUtil.Range range = new EditingUtil.Range();
431         String wordToBeReplaced = EditingUtil.getWordAtCursor(ic, wordSeparators, range);
432         // If we enable phrase-based alternatives, only send up the first word
433         // in suggestion and wordToBeReplaced.
434         mLogger.textModifiedByChooseSuggestion(suggestion.length(), wordToBeReplaced.length(),
435                                                index, wordToBeReplaced, suggestion);
436     }
437 
logKeyboardWarningDialogShown()438     public void logKeyboardWarningDialogShown() {
439         mLogger.keyboardWarningDialogShown();
440     }
441 
logKeyboardWarningDialogDismissed()442     public void logKeyboardWarningDialogDismissed() {
443         mLogger.keyboardWarningDialogDismissed();
444     }
445 
logKeyboardWarningDialogOk()446     public void logKeyboardWarningDialogOk() {
447         mLogger.keyboardWarningDialogOk();
448     }
449 
logKeyboardWarningDialogCancel()450     public void logKeyboardWarningDialogCancel() {
451         mLogger.keyboardWarningDialogCancel();
452     }
453 
logSwipeHintDisplayed()454     public void logSwipeHintDisplayed() {
455         mLogger.swipeHintDisplayed();
456     }
457 
logPunctuationHintDisplayed()458     public void logPunctuationHintDisplayed() {
459         mLogger.punctuationHintDisplayed();
460     }
461 
logVoiceInputDelivered(int length)462     public void logVoiceInputDelivered(int length) {
463         mLogger.voiceInputDelivered(length);
464     }
465 
logInputEnded()466     public void logInputEnded() {
467         mLogger.inputEnded();
468     }
469 
flushLogs()470     public void flushLogs() {
471         mLogger.flush();
472     }
473 
makeIntent()474     private static Intent makeIntent() {
475         Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
476 
477         // On Cupcake, use VoiceIMEHelper since VoiceSearch doesn't support.
478         // On Donut, always use VoiceSearch, since VoiceIMEHelper and
479         // VoiceSearch may conflict.
480         if (Build.VERSION.RELEASE.equals("1.5")) {
481             intent = intent.setClassName(
482               "com.google.android.voiceservice",
483               "com.google.android.voiceservice.IMERecognitionService");
484         } else {
485             intent = intent.setClassName(
486               "com.google.android.voicesearch",
487               "com.google.android.voicesearch.RecognitionService");
488         }
489 
490         return intent;
491     }
492 
493     /**
494      * Cancel in-progress speech recognition.
495      */
cancel()496     public void cancel() {
497         switch (mState) {
498         case LISTENING:
499             mLogger.cancelDuringListening();
500             break;
501         case WORKING:
502             mLogger.cancelDuringWorking();
503             break;
504         case ERROR:
505             mLogger.cancelDuringError();
506             break;
507         }
508         mState = DEFAULT;
509 
510         // Remove all pending tasks (e.g., timers to cancel voice input)
511         mHandler.removeMessages(MSG_CLOSE_ERROR_DIALOG);
512 
513         mSpeechRecognizer.cancel();
514         mUiListener.onCancelVoice();
515         mRecognitionView.finish();
516     }
517 
getErrorStringId(int errorType, boolean endpointed)518     private int getErrorStringId(int errorType, boolean endpointed) {
519         switch (errorType) {
520             // We use CLIENT_ERROR to signify that voice search is not available on the device.
521             case SpeechRecognizer.ERROR_CLIENT:
522                 return R.string.voice_not_installed;
523             case SpeechRecognizer.ERROR_NETWORK:
524                 return R.string.voice_network_error;
525             case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
526                 return endpointed ?
527                         R.string.voice_network_error : R.string.voice_too_much_speech;
528             case SpeechRecognizer.ERROR_AUDIO:
529                 return R.string.voice_audio_error;
530             case SpeechRecognizer.ERROR_SERVER:
531                 return R.string.voice_server_error;
532             case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
533                 return R.string.voice_speech_timeout;
534             case SpeechRecognizer.ERROR_NO_MATCH:
535                 return R.string.voice_no_match;
536             default: return R.string.voice_error;
537         }
538     }
539 
onError(int errorType, boolean endpointed)540     private void onError(int errorType, boolean endpointed) {
541         Log.i(TAG, "error " + errorType);
542         mLogger.error(errorType);
543         onError(mContext.getString(getErrorStringId(errorType, endpointed)));
544     }
545 
onError(String error)546     private void onError(String error) {
547         mState = ERROR;
548         mRecognitionView.showError(error);
549         // Wait a couple seconds and then automatically dismiss message.
550         mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_CLOSE_ERROR_DIALOG), 2000);
551     }
552 
553     private class ImeRecognitionListener implements RecognitionListener {
554         // Waveform data
555         final ByteArrayOutputStream mWaveBuffer = new ByteArrayOutputStream();
556         int mSpeechStart;
557         private boolean mEndpointed = false;
558 
onReadyForSpeech(Bundle noiseParams)559         public void onReadyForSpeech(Bundle noiseParams) {
560             mRecognitionView.showListening();
561         }
562 
onBeginningOfSpeech()563         public void onBeginningOfSpeech() {
564             mEndpointed = false;
565             mSpeechStart = mWaveBuffer.size();
566         }
567 
onRmsChanged(float rmsdB)568         public void onRmsChanged(float rmsdB) {
569             mRecognitionView.updateVoiceMeter(rmsdB);
570         }
571 
onBufferReceived(byte[] buf)572         public void onBufferReceived(byte[] buf) {
573             try {
574                 mWaveBuffer.write(buf);
575             } catch (IOException e) {}
576         }
577 
onEndOfSpeech()578         public void onEndOfSpeech() {
579             mEndpointed = true;
580             mState = WORKING;
581             mRecognitionView.showWorking(mWaveBuffer, mSpeechStart, mWaveBuffer.size());
582         }
583 
onError(int errorType)584         public void onError(int errorType) {
585             mState = ERROR;
586             VoiceInput.this.onError(errorType, mEndpointed);
587         }
588 
onResults(Bundle resultsBundle)589         public void onResults(Bundle resultsBundle) {
590             List<String> results = resultsBundle
591                     .getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
592             // VS Market update is needed for IME froyo clients to access the alternatesBundle
593             // TODO: verify this.
594             Bundle alternatesBundle = resultsBundle.getBundle(ALTERNATES_BUNDLE);
595             mState = DEFAULT;
596 
597             final Map<String, List<CharSequence>> alternatives =
598                 new HashMap<String, List<CharSequence>>();
599 
600             if (ENABLE_WORD_CORRECTIONS && alternatesBundle != null && results.size() > 0) {
601                 // Use the top recognition result to map each alternative's start:length to a word.
602                 String[] words = results.get(0).split(" ");
603                 Bundle spansBundle = alternatesBundle.getBundle(AlternatesBundleKeys.SPANS);
604                 for (String key : spansBundle.keySet()) {
605                     // Get the word for which these alternates correspond to.
606                     Bundle spanBundle = spansBundle.getBundle(key);
607                     int start = spanBundle.getInt(AlternatesBundleKeys.START);
608                     int length = spanBundle.getInt(AlternatesBundleKeys.LENGTH);
609                     // Only keep single-word based alternatives.
610                     if (length == 1 && start < words.length) {
611                         // Get the alternatives associated with the span.
612                         // If a word appears twice in a recognition result,
613                         // concatenate the alternatives for the word.
614                         List<CharSequence> altList = alternatives.get(words[start]);
615                         if (altList == null) {
616                             altList = new ArrayList<CharSequence>();
617                             alternatives.put(words[start], altList);
618                         }
619                         Parcelable[] alternatesArr = spanBundle
620                             .getParcelableArray(AlternatesBundleKeys.ALTERNATES);
621                         for (int j = 0; j < alternatesArr.length &&
622                                  altList.size() < MAX_ALT_LIST_LENGTH; j++) {
623                             Bundle alternateBundle = (Bundle) alternatesArr[j];
624                             String alternate = alternateBundle.getString(AlternatesBundleKeys.TEXT);
625                             // Don't allow duplicates in the alternates list.
626                             if (!altList.contains(alternate)) {
627                                 altList.add(alternate);
628                             }
629                         }
630                     }
631                 }
632             }
633 
634             if (results.size() > 5) {
635                 results = results.subList(0, 5);
636             }
637             mUiListener.onVoiceResults(results, alternatives);
638             mRecognitionView.finish();
639         }
640 
onPartialResults(final Bundle partialResults)641         public void onPartialResults(final Bundle partialResults) {
642             // currently - do nothing
643         }
644 
onEvent(int eventType, Bundle params)645         public void onEvent(int eventType, Bundle params) {
646             // do nothing - reserved for events that might be added in the future
647         }
648     }
649 }
650