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