Home | History | Annotate | Download | only in voice
      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 
     17 package com.android.inputmethod.deprecated.voice;
     18 
     19 import com.android.inputmethod.latin.EditingUtils;
     20 import com.android.inputmethod.latin.LatinImeLogger;
     21 import com.android.inputmethod.latin.R;
     22 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
     23 
     24 import android.content.ContentResolver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.res.Configuration;
     28 import android.os.Build;
     29 import android.os.Bundle;
     30 import android.os.Message;
     31 import android.os.Parcelable;
     32 import android.speech.RecognitionListener;
     33 import android.speech.RecognizerIntent;
     34 import android.speech.SpeechRecognizer;
     35 import android.util.Log;
     36 import android.view.View;
     37 import android.view.View.OnClickListener;
     38 import android.view.inputmethod.InputConnection;
     39 
     40 import java.io.ByteArrayOutputStream;
     41 import java.io.IOException;
     42 import java.util.ArrayList;
     43 import java.util.HashMap;
     44 import java.util.List;
     45 import java.util.Locale;
     46 import 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  */
     55 public 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