Refactor to move voice functionarities in LatinIME to VoiceIMEConnector

Change-Id: I593a8187d48338c9c0e7d75c73c2dbfc32400335
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 9c7af35..d624ae4 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -17,8 +17,7 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.latin.LatinIMEUtil.RingCharBuffer;
-import com.android.inputmethod.voice.FieldContext;
-import com.android.inputmethod.voice.VoiceInput;
+import com.android.inputmethod.voice.VoiceIMEConnector;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -41,8 +40,6 @@
 import android.os.Vibrator;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
-import android.speech.SpeechRecognizer;
-import android.text.ClipboardManager;
 import android.text.TextUtils;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -50,10 +47,7 @@
 import android.util.Printer;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.inputmethod.CompletionInfo;
@@ -70,25 +64,20 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 
 /**
  * Input method implementation for Qwerty'ish keyboard.
  */
 public class LatinIME extends InputMethodService
         implements BaseKeyboardView.OnKeyboardActionListener,
-        VoiceInput.UiListener,
         SharedPreferences.OnSharedPreferenceChangeListener,
         Tutorial.TutorialListener {
     private static final String TAG = "LatinIME";
     private static final boolean PERF_DEBUG = false;
-    static final boolean DEBUG = false;
-    static final boolean TRACE = false;
-    static final boolean VOICE_INSTALLED = true;
-    static final boolean ENABLE_VOICE_BUTTON = true;
+    private static final boolean DEBUG = false;
+    private static final boolean TRACE = false;
 
     private static final String PREF_SOUND_ON = "sound_on";
     private static final String PREF_POPUP_ON = "popup_on";
@@ -97,21 +86,6 @@
     private static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
     private static final String PREF_AUTO_COMPLETION_THRESHOLD = "auto_completion_threshold";
     private static final String PREF_BIGRAM_SUGGESTIONS = "bigram_suggestion";
-    private static final String PREF_VOICE_MODE = "voice_mode";
-
-    // Whether or not the user has used voice input before (and thus, whether to show the
-    // first-run warning dialog or not).
-    private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
-
-    // Whether or not the user has used voice input from an unsupported locale UI before.
-    // For example, the user has a Chinese UI but activates voice input.
-    private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
-            "has_used_voice_input_unsupported_locale";
-
-    // The private IME option used to indicate that no microphone should be shown for a
-    // given text field. For instance this is specified by the search dialog when the
-    // dialog is already showing a voice search button.
-    private static final String IME_OPTION_NO_MICROPHONE = "nm";
 
     public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
     public static final String PREF_INPUT_LANGUAGE = "input_language";
@@ -156,29 +130,22 @@
     private CompletionInfo[] mCompletions;
 
     private AlertDialog mOptionsDialog;
-    private AlertDialog mVoiceWarningDialog;
 
     private KeyboardSwitcher mKeyboardSwitcher;
     private SubtypeSwitcher mSubtypeSwitcher;
+    private VoiceIMEConnector mVoiceConnector;
 
     private UserDictionary mUserDictionary;
     private UserBigramDictionary mUserBigramDictionary;
     private ContactsDictionary mContactsDictionary;
     private AutoDictionary mAutoDictionary;
 
-    private Hints mHints;
-
     private Resources mResources;
 
     private final StringBuilder mComposing = new StringBuilder();
     private WordComposer mWord = new WordComposer();
     private int mCommittedLength;
     private boolean mPredicting;
-    private boolean mRecognizing;
-    private boolean mAfterVoiceInput;
-    private boolean mImmediatelyAfterVoiceInput;
-    private boolean mShowingVoiceSuggestions;
-    private boolean mVoiceInputHighlighted;
     private CharSequence mBestWord;
     private boolean mPredictionOn;
     private boolean mCompletionOn;
@@ -189,19 +156,13 @@
     private boolean mReCorrectionEnabled;
     private boolean mBigramSuggestionEnabled;
     private boolean mAutoCorrectOn;
-    private boolean mPasswordText;
     private boolean mVibrateOn;
     private boolean mSoundOn;
     private boolean mPopupOn;
     private boolean mAutoCap;
     private boolean mQuickFixes;
-    private boolean mHasUsedVoiceInput;
-    private boolean mHasUsedVoiceInputUnsupportedLocale;
-    private boolean mLocaleSupportedForVoiceInput;
-    private boolean mIsShowingHint;
     private int     mCorrectionMode;
-    private boolean mVoiceKeyEnabled;
-    private boolean mVoiceButtonOnPrimary;
+
     private int     mOrientation;
     private List<CharSequence> mSuggestPuncList;
     // Keep track of the last selection range to decide if we need to show word alternatives
@@ -227,25 +188,15 @@
     /* package */ String mWordSeparators;
     private String mSentenceSeparators;
     private String mSuggestPuncs;
-    private VoiceInput mVoiceInput;
-    private final VoiceResults mVoiceResults = new VoiceResults();
+    // TODO: Move this flag to VoiceIMEConnector
     private boolean mConfigurationChanging;
 
     // Keeps track of most recently inserted text (multi-character key) for reverting
     private CharSequence mEnteredText;
     private boolean mRefreshKeyboardRequired;
 
-    // For each word, a list of potential replacements, usually from voice.
-    private final Map<String, List<CharSequence>> mWordToSuggestions =
-            new HashMap<String, List<CharSequence>>();
-
     private final ArrayList<WordAlternatives> mWordHistory = new ArrayList<WordAlternatives>();
 
-    private class VoiceResults {
-        List<String> candidates;
-        Map<String, List<CharSequence>> alternatives;
-    }
-
     public abstract static class WordAlternatives {
         protected CharSequence mChosenWord;
 
@@ -294,9 +245,9 @@
         }
     }
 
-    /* package */ UIHandler mHandler = new UIHandler();
+    /* package */ final UIHandler mHandler = new UIHandler();
 
-    /* package */ class UIHandler extends Handler {
+    public class UIHandler extends Handler {
         private static final int MSG_UPDATE_SUGGESTIONS = 0;
         private static final int MSG_UPDATE_OLD_SUGGESTIONS = 1;
         private static final int MSG_UPDATE_SHIFT_STATE = 2;
@@ -316,7 +267,9 @@
                 mKeyboardSwitcher.updateShiftState();
                 break;
             case MSG_VOICE_RESULTS:
-                handleVoiceResults();
+                mVoiceConnector.handleVoiceResults(mKeyboardSwitcher, preferCapitalization()
+                        || (mKeyboardSwitcher.isAlphabetMode()
+                                && mKeyboardSwitcher.isShiftedOrShiftLocked()));
                 break;
             case MSG_START_TUTORIAL:
                 if (mTutorial == null) {
@@ -405,19 +358,7 @@
         // register to receive ringer mode changes for silent mode
         IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
         registerReceiver(mReceiver, filter);
-        if (VOICE_INSTALLED) {
-            mVoiceInput = new VoiceInput(this, this);
-            mHints = new Hints(this, new Hints.Display() {
-                public void showHint(int viewResource) {
-                    LayoutInflater inflater = (LayoutInflater) getSystemService(
-                            Context.LAYOUT_INFLATER_SERVICE);
-                    View view = inflater.inflate(viewResource, null);
-                    setCandidatesView(view);
-                    setCandidatesViewShown(true);
-                    mIsShowingHint = true;
-                }
-              });
-        }
+        mVoiceConnector = VoiceIMEConnector.init(this, mHandler);
         prefs.registerOnSharedPreferenceChangeListener(this);
     }
 
@@ -508,9 +449,7 @@
             mContactsDictionary.close();
         }
         unregisterReceiver(mReceiver);
-        if (VOICE_INSTALLED && mVoiceInput != null) {
-            mVoiceInput.destroy();
-        }
+        mVoiceConnector.destroy();
         LatinImeLogger.commit();
         LatinImeLogger.onDestroy();
         super.onDestroy();
@@ -531,15 +470,14 @@
             final int mode = mKeyboardSwitcher.getKeyboardMode();
             final EditorInfo attribute = getCurrentInputEditorInfo();
             final int imeOptions = (attribute != null) ? attribute.imeOptions : 0;
-            mKeyboardSwitcher.loadKeyboard(mode, imeOptions, mVoiceKeyEnabled,
-                    mVoiceButtonOnPrimary);
+            mKeyboardSwitcher.loadKeyboard(mode, imeOptions,
+                    mVoiceConnector.isVoiceButtonEnabled(),
+                    mVoiceConnector.isVoiceButtonOnPrimary());
         }
 
         mConfigurationChanging = true;
         super.onConfigurationChanged(conf);
-        if (mRecognizing) {
-            switchToRecognitionStatusView();
-        }
+        mVoiceConnector.onConfigurationChanged(mConfigurationChanging);
         mConfigurationChanging = false;
     }
 
@@ -591,16 +529,8 @@
         // Most such things we decide below in the switch statement, but we need to know
         // now whether this is a password text field, because we need to know now (before
         // the switch statement) whether we want to enable the voice button.
-        mPasswordText = false;
         int variation = attribute.inputType & EditorInfo.TYPE_MASK_VARIATION;
-        if (isPasswordVariation(variation)) {
-            mPasswordText = true;
-        }
-
-        mAfterVoiceInput = false;
-        mImmediatelyAfterVoiceInput = false;
-        mShowingVoiceSuggestions = false;
-        mVoiceInputHighlighted = false;
+        mVoiceConnector.resetVoiceStates(isPasswordVariation(variation));
         mInputTypeNoAutoCorrect = false;
         mPredictionOn = false;
         mCompletionOn = false;
@@ -679,7 +609,8 @@
         mJustAddedAutoSpace = false;
 
         loadSettings(attribute);
-        switcher.loadKeyboard(mode, attribute.imeOptions, mVoiceKeyEnabled, mVoiceButtonOnPrimary);
+        switcher.loadKeyboard(mode, attribute.imeOptions, mVoiceConnector.isVoiceButtonEnabled(),
+                mVoiceConnector.isVoiceButtonOnPrimary());
         switcher.updateShiftState();
 
         setCandidatesViewShownInternal(isCandidateStripVisible(),
@@ -731,14 +662,8 @@
         LatinImeLogger.commit();
         onAutoCompletionStateChanged(false);
 
-        if (VOICE_INSTALLED && !mConfigurationChanging) {
-            if (mAfterVoiceInput) {
-                mVoiceInput.flushAllTextModificationCounters();
-                mVoiceInput.logInputEnded();
-            }
-            mVoiceInput.flushLogs();
-            mVoiceInput.cancel();
-        }
+        mVoiceConnector.flushVoiceInputLogs(mConfigurationChanging);
+
         BaseKeyboardView inputView = mKeyboardSwitcher.getInputView();
         if (inputView != null)
             inputView.closing();
@@ -760,13 +685,7 @@
     @Override
     public void onUpdateExtractedText(int token, ExtractedText text) {
         super.onUpdateExtractedText(token, text);
-        InputConnection ic = getCurrentInputConnection();
-        if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
-            if (mHints.showPunctuationHintIfNecessary(ic)) {
-                mVoiceInput.logPunctuationHintDisplayed();
-            }
-        }
-        mImmediatelyAfterVoiceInput = false;
+        mVoiceConnector.showPunctuationHintIfNecessary();
     }
 
     @Override
@@ -785,14 +704,12 @@
                     + ", ce=" + candidatesEnd);
         }
 
-        if (mAfterVoiceInput) {
-            mVoiceInput.setCursorPos(newSelEnd);
-            mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
-        }
+        mVoiceConnector.setCursorAndSelection(newSelEnd, newSelStart);
 
         // If the current selection in the text view changes, we should
         // clear whatever candidate text we have.
-        if ((((mComposing.length() > 0 && mPredicting) || mVoiceInputHighlighted)
+        if ((((mComposing.length() > 0 && mPredicting)
+                || mVoiceConnector.isVoiceInputHighlighted())
                 && (newSelStart != candidatesEnd
                     || newSelEnd != candidatesEnd)
                 && mLastSelectionStart != newSelStart)) {
@@ -804,7 +721,7 @@
             if (ic != null) {
                 ic.finishComposingText();
             }
-            mVoiceInputHighlighted = false;
+            mVoiceConnector.setVoiceInputHighlighted(false);
         } else if (!mPredicting && !mJustAccepted) {
             switch (TextEntryState.getState()) {
                 case ACCEPTED_DEFAULT:
@@ -829,8 +746,7 @@
                 if (isPredictionOn() && !mJustReverted
                         && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
                                 || TextEntryState.isCorrecting())
-                                && (newSelStart < newSelEnd - 1 || (!mPredicting))
-                                && !mVoiceInputHighlighted) {
+                                && (newSelStart < newSelEnd - 1 || (!mPredicting))) {
                     if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
                         mHandler.postUpdateOldSuggestions();
                     } else {
@@ -888,18 +804,7 @@
             mOptionsDialog.dismiss();
             mOptionsDialog = null;
         }
-        if (!mConfigurationChanging) {
-            if (mAfterVoiceInput) mVoiceInput.logInputEnded();
-            if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
-                mVoiceInput.logKeyboardWarningDialogDismissed();
-                mVoiceWarningDialog.dismiss();
-                mVoiceWarningDialog = null;
-            }
-            if (VOICE_INSTALLED & mRecognizing) {
-                mVoiceInput.cancel();
-            }
-        }
-        mWordToSuggestions.clear();
+        mVoiceConnector.hideVoiceWindow(mConfigurationChanging);
         mWordHistory.clear();
         super.hideWindow();
         TextEntryState.endSession();
@@ -1019,21 +924,7 @@
         return super.onKeyUp(keyCode, event);
     }
 
-    private void revertVoiceInput() {
-        InputConnection ic = getCurrentInputConnection();
-        if (ic != null) ic.commitText("", 1);
-        updateSuggestions();
-        mVoiceInputHighlighted = false;
-    }
-
-    private void commitVoiceInput() {
-        InputConnection ic = getCurrentInputConnection();
-        if (ic != null) ic.finishComposingText();
-        updateSuggestions();
-        mVoiceInputHighlighted = false;
-    }
-
-    private void commitTyped(InputConnection inputConnection) {
+    public void commitTyped(InputConnection inputConnection) {
         if (mPredicting) {
             mPredicting = false;
             if (mComposing.length() > 0) {
@@ -1222,10 +1113,9 @@
         case LatinKeyboardView.KEYCODE_CAPSLOCK:
             switcher.toggleCapsLock();
             break;
-        case LatinKeyboardView.KEYCODE_VOICE:
-            if (VOICE_INSTALLED) {
-                startListening(false /* was a button press, was not a swipe */);
-            }
+        case LatinKeyboardView.KEYCODE_VOICE: /* was a button press, was not a swipe */
+            mVoiceConnector.startListening(false,
+                    mKeyboardSwitcher.getInputView().getWindowToken(), mConfigurationChanging);
             break;
         case KEYCODE_TAB:
             handleTab();
@@ -1250,9 +1140,7 @@
     }
 
     public void onText(CharSequence text) {
-        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
-            commitVoiceInput();
-        }
+        mVoiceConnector.commitVoiceInput();
         InputConnection ic = getCurrentInputConnection();
         if (ic == null) return;
         abortCorrection(false);
@@ -1274,29 +1162,14 @@
     }
 
     private void handleBackspace() {
-        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
-            mVoiceInput.incrementTextModificationDeleteCount(
-                    mVoiceResults.candidates.get(0).toString().length());
-            revertVoiceInput();
-            return;
-        }
+        if (mVoiceConnector.logAndRevertVoiceInput()) return;
         boolean deleteChar = false;
         InputConnection ic = getCurrentInputConnection();
         if (ic == null) return;
 
         ic.beginBatchEdit();
 
-        if (mAfterVoiceInput) {
-            // Don't log delete if the user is pressing delete at
-            // the beginning of the text box (hence not deleting anything)
-            if (mVoiceInput.getCursorPos() > 0) {
-                // If anything was selected before the delete was pressed, increment the
-                // delete count by the length of the selection
-                int deleteLen  =  mVoiceInput.getSelectionSpan() > 0 ?
-                        mVoiceInput.getSelectionSpan() : 1;
-                mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
-            }
-        }
+        mVoiceConnector.handleBackspace();
 
         if (mPredicting) {
             final int length = mComposing.length();
@@ -1377,14 +1250,8 @@
     }
 
     private void handleCharacter(int primaryCode, int[] keyCodes) {
-        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
-            commitVoiceInput();
-        }
+        mVoiceConnector.handleCharacter();
 
-        if (mAfterVoiceInput) {
-            // Assume input length is 1. This assumption fails for smiley face insertions.
-            mVoiceInput.incrementTextModificationInsertCount(1);
-        }
         if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) {
             abortCorrection(false);
         }
@@ -1441,14 +1308,7 @@
     }
 
     private void handleSeparator(int primaryCode) {
-        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
-            commitVoiceInput();
-        }
-
-        if (mAfterVoiceInput){
-            // Assume input length is 1. This assumption fails for smiley face insertions.
-            mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
-        }
+        mVoiceConnector.handleSeparator();
 
         // Should dismiss the "Touch again to save" message when handling separator
         if (mCandidateView != null && mCandidateView.dismissAddToDictionaryHint()) {
@@ -1509,9 +1369,7 @@
 
     private void handleClose() {
         commitTyped(getCurrentInputConnection());
-        if (VOICE_INSTALLED & mRecognizing) {
-            mVoiceInput.cancel();
-        }
+        mVoiceConnector.handleClose();
         requestHideSelf(0);
         LatinKeyboardView inputView = mKeyboardSwitcher.getInputView();
         if (inputView != null)
@@ -1557,16 +1415,9 @@
                 && (isPredictionOn() || mCompletionOn || isShowingPunctuationList()));
     }
 
-    public void onCancelVoice() {
-        if (mRecognizing) {
-            switchToKeyboardView();
-        }
-    }
-
-    private void switchToKeyboardView() {
+    public void switchToKeyboardView() {
       mHandler.post(new Runnable() {
           public void run() {
-              mRecognizing = false;
               if (mKeyboardSwitcher.getInputView() != null) {
                   setInputView(mKeyboardSwitcher.getInputView());
               }
@@ -1576,175 +1427,18 @@
           }});
     }
 
-    private void switchToRecognitionStatusView() {
-        final boolean configChanged = mConfigurationChanging;
-        mHandler.post(new Runnable() {
-            public void run() {
-                setCandidatesViewShown(false);
-                mRecognizing = true;
-                View v = mVoiceInput.getView();
-                ViewParent p = v.getParent();
-                if (p != null && p instanceof ViewGroup) {
-                    ((ViewGroup)v.getParent()).removeView(v);
-                }
-                setInputView(v);
-                updateInputViewShown();
-                if (configChanged) {
-                    mVoiceInput.onConfigurationChanged();
-                }
-        }});
-    }
-
-    private void startListening(boolean swipe) {
-        if (!mHasUsedVoiceInput ||
-                (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) {
-            // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
-            showVoiceWarningDialog(swipe);
-        } else {
-            reallyStartListening(swipe);
-        }
-    }
-
-    private void reallyStartListening(boolean swipe) {
-        if (!mHasUsedVoiceInput) {
-            // The user has started a voice input, so remember that in the
-            // future (so we don't show the warning dialog after the first run).
-            SharedPreferences.Editor editor =
-                    PreferenceManager.getDefaultSharedPreferences(this).edit();
-            editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
-            SharedPreferencesCompat.apply(editor);
-            mHasUsedVoiceInput = true;
-        }
-
-        if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
-            // The user has started a voice input from an unsupported locale, so remember that
-            // in the future (so we don't show the warning dialog the next time they do this).
-            SharedPreferences.Editor editor =
-                    PreferenceManager.getDefaultSharedPreferences(this).edit();
-            editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
-            SharedPreferencesCompat.apply(editor);
-            mHasUsedVoiceInputUnsupportedLocale = true;
-        }
-
-        // Clear N-best suggestions
-        clearSuggestions();
-
-        FieldContext context = makeFieldContext();
-        mVoiceInput.startListening(context, swipe);
-        switchToRecognitionStatusView();
-    }
-
-    private void showVoiceWarningDialog(final boolean swipe) {
-        AlertDialog.Builder builder = new AlertDialog.Builder(this);
-        builder.setCancelable(true);
-        builder.setIcon(R.drawable.ic_mic_dialog);
-        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
-            public void onClick(DialogInterface dialog, int whichButton) {
-                mVoiceInput.logKeyboardWarningDialogOk();
-                reallyStartListening(swipe);
-            }
-        });
-        builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
-            public void onClick(DialogInterface dialog, int whichButton) {
-                mVoiceInput.logKeyboardWarningDialogCancel();
-            }
-        });
-
-        if (mLocaleSupportedForVoiceInput) {
-            String message = getString(R.string.voice_warning_may_not_understand) + "\n\n" +
-                    getString(R.string.voice_warning_how_to_turn_off);
-            builder.setMessage(message);
-        } else {
-            String message = getString(R.string.voice_warning_locale_not_supported) + "\n\n" +
-                    getString(R.string.voice_warning_may_not_understand) + "\n\n" +
-                    getString(R.string.voice_warning_how_to_turn_off);
-            builder.setMessage(message);
-        }
-
-        builder.setTitle(R.string.voice_warning_title);
-        mVoiceWarningDialog = builder.create();
-
-        Window window = mVoiceWarningDialog.getWindow();
-        WindowManager.LayoutParams lp = window.getAttributes();
-        lp.token = mKeyboardSwitcher.getInputView().getWindowToken();
-        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
-        window.setAttributes(lp);
-        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
-        mVoiceInput.logKeyboardWarningDialogShown();
-        mVoiceWarningDialog.show();
-    }
-
-    public void onVoiceResults(List<String> candidates,
-            Map<String, List<CharSequence>> alternatives) {
-        if (!mRecognizing) {
-            return;
-        }
-        mVoiceResults.candidates = candidates;
-        mVoiceResults.alternatives = alternatives;
-        mHandler.updateVoiceResults();
-    }
-
-    private void handleVoiceResults() {
-        mAfterVoiceInput = true;
-        mImmediatelyAfterVoiceInput = true;
-
-        InputConnection ic = getCurrentInputConnection();
-        if (!isFullscreenMode()) {
-            // Start listening for updates to the text from typing, etc.
-            if (ic != null) {
-                ExtractedTextRequest req = new ExtractedTextRequest();
-                ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
-            }
-        }
-
-        vibrate();
-        switchToKeyboardView();
-
-        final List<CharSequence> nBest = new ArrayList<CharSequence>();
-        KeyboardSwitcher switcher = mKeyboardSwitcher;
-        boolean capitalizeFirstWord = preferCapitalization()
-                || (switcher.isAlphabetMode() && switcher.isShiftedOrShiftLocked());
-        for (String c : mVoiceResults.candidates) {
-            if (capitalizeFirstWord) {
-                c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
-            }
-            nBest.add(c);
-        }
-
-        if (nBest.size() == 0) {
-            return;
-        }
-
-        String bestResult = nBest.get(0).toString();
-
-        mVoiceInput.logVoiceInputDelivered(bestResult.length());
-
-        mHints.registerVoiceResult(bestResult);
-
-        if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
-
-        commitTyped(ic);
-        EditingUtil.appendText(ic, bestResult);
-
-        if (ic != null) ic.endBatchEdit();
-
-        mVoiceInputHighlighted = true;
-        mWordToSuggestions.putAll(mVoiceResults.alternatives);
-    }
-
-    private void clearSuggestions() {
+    public void clearSuggestions() {
         setSuggestions(null, false, false, false);
     }
 
-    private void setSuggestions(
+    public void setSuggestions(
             List<CharSequence> suggestions,
             boolean completions,
             boolean typedWordValid,
             boolean haveMinimalSuggestion) {
 
-        if (mIsShowingHint) {
+        if (mVoiceConnector.getAndResetIsShowingHint()) {
              setCandidatesView(mCandidateViewContainer);
-             mIsShowingHint = false;
         }
 
         if (mCandidateView != null) {
@@ -1753,11 +1447,12 @@
         }
     }
 
-    private void updateSuggestions() {
+    public void updateSuggestions() {
         mKeyboardSwitcher.setPreferredLetters(null);
 
         // Check if we have a suggestion engine attached.
-        if ((mSuggest == null || !isPredictionOn()) && !mVoiceInputHighlighted) {
+        if ((mSuggest == null || !isPredictionOn())
+                && !mVoiceConnector.isVoiceInputHighlighted()) {
             return;
         }
 
@@ -1846,13 +1541,7 @@
 
     public void pickSuggestionManually(int index, CharSequence suggestion) {
         List<CharSequence> suggestions = mCandidateView.getSuggestions();
-        if (mAfterVoiceInput && mShowingVoiceSuggestions) {
-            mVoiceInput.flushAllTextModificationCounters();
-            // send this intent AFTER logging any prior aggregated edits.
-            mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
-                                                          mWordSeparators,
-                                                          getCurrentInputConnection());
-        }
+        mVoiceConnector.flushAndLogAllTextModificationCounters(index, suggestion, mWordSeparators);
 
         final boolean correcting = TextEntryState.isCorrecting();
         InputConnection ic = getCurrentInputConnection();
@@ -1932,27 +1621,6 @@
         }
     }
 
-    private void rememberReplacedWord(CharSequence suggestion) {
-        if (mShowingVoiceSuggestions) {
-            // Retain the replaced word in the alternatives array.
-            EditingUtil.Range range = new EditingUtil.Range();
-            String wordToBeReplaced = EditingUtil.getWordAtCursor(getCurrentInputConnection(),
-                    mWordSeparators, range);
-            if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
-                wordToBeReplaced = wordToBeReplaced.toLowerCase();
-            }
-            if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
-                List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
-                if (suggestions.contains(suggestion)) {
-                    suggestions.remove(suggestion);
-                }
-                suggestions.add(wordToBeReplaced);
-                mWordToSuggestions.remove(wordToBeReplaced);
-                mWordToSuggestions.put(suggestion.toString(), suggestions);
-            }
-        }
-    }
-
     /**
      * Commits the chosen word to the text field and saves it for later
      * retrieval.
@@ -1967,7 +1635,7 @@
             return;
         InputConnection ic = getCurrentInputConnection();
         if (ic != null) {
-            rememberReplacedWord(suggestion);
+            mVoiceConnector.rememberReplacedWord(suggestion, mWordSeparators);
             ic.commitText(suggestion, 1);
         }
         saveWordInHistory(suggestion);
@@ -1982,38 +1650,6 @@
     }
 
     /**
-     * Tries to apply any voice alternatives for the word if this was a spoken word and
-     * there are voice alternatives.
-     * @param touching The word that the cursor is touching, with position information
-     * @return true if an alternative was found, false otherwise.
-     */
-    private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) {
-        // Search for result in spoken word alternatives
-        String selectedWord = touching.word.toString().trim();
-        if (!mWordToSuggestions.containsKey(selectedWord)) {
-            selectedWord = selectedWord.toLowerCase();
-        }
-        if (mWordToSuggestions.containsKey(selectedWord)) {
-            mShowingVoiceSuggestions = true;
-            List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
-            // If the first letter of touching is capitalized, make all the suggestions
-            // start with a capital letter.
-            if (Character.isUpperCase(touching.word.charAt(0))) {
-                for (int i = 0; i < suggestions.size(); i++) {
-                    String origSugg = (String) suggestions.get(i);
-                    String capsSugg = origSugg.toUpperCase().charAt(0)
-                            + origSugg.subSequence(1, origSugg.length()).toString();
-                    suggestions.set(i, capsSugg);
-                }
-            }
-            setSuggestions(suggestions, false, true, true);
-            setCandidatesViewShown(true);
-            return true;
-        }
-        return false;
-    }
-
-    /**
      * Tries to apply any typed alternatives for the word if we have any cached alternatives,
      * otherwise tries to find new corrections and completions for the word.
      * @param touching The word that the cursor is touching, with position information
@@ -2061,7 +1697,7 @@
     }
 
     private void setOldSuggestions() {
-        mShowingVoiceSuggestions = false;
+        mVoiceConnector.setShowingVoiceSuggestions(false);
         if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) {
             return;
         }
@@ -2075,7 +1711,8 @@
             if (touching != null && touching.word.length() > 1) {
                 ic.beginBatchEdit();
 
-                if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) {
+                if (!mVoiceConnector.applyVoiceAlternatives(touching)
+                        && !applyTypedAlternatives(touching)) {
                     abortCorrection(true);
                 } else {
                     TextEntryState.selectedForCorrection();
@@ -2221,8 +1858,8 @@
         final int mode = switcher.getKeyboardMode();
         final EditorInfo attribute = getCurrentInputEditorInfo();
         final int imeOptions = (attribute != null) ? attribute.imeOptions : 0;
-        switcher.loadKeyboard(mode, imeOptions, mVoiceKeyEnabled,
-                mVoiceButtonOnPrimary);
+        switcher.loadKeyboard(mode, imeOptions, mVoiceConnector.isVoiceButtonEnabled(),
+                mVoiceConnector.isVoiceButtonOnPrimary());
         initSuggest();
         switcher.updateShiftState();
     }
@@ -2240,8 +1877,8 @@
 
     public void swipeRight() {
         if (LatinKeyboardView.DEBUG_AUTO_PLAY) {
-            ClipboardManager cm = ((ClipboardManager)getSystemService(CLIPBOARD_SERVICE));
-            CharSequence text = cm.getText();
+            CharSequence text = ((android.text.ClipboardManager)getSystemService(
+                    CLIPBOARD_SERVICE)).getText();
             if (!TextUtils.isEmpty(text)) {
                 mKeyboardSwitcher.getInputView().startPlaying(text.toString());
             }
@@ -2284,26 +1921,6 @@
         }
     }
 
-    private FieldContext makeFieldContext() {
-        return new FieldContext(
-                getCurrentInputConnection(),
-                getCurrentInputEditorInfo(),
-                mSubtypeSwitcher.getInputLocaleStr(),
-                mSubtypeSwitcher.getEnabledLanguages());
-    }
-
-    private boolean fieldCanDoVoice(FieldContext fieldContext) {
-        return !mPasswordText
-                && mVoiceInput != null
-                && !mVoiceInput.isBlacklistedField(fieldContext);
-    }
-
-    private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
-        return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext)
-                && !(attribute != null
-                        && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions))
-                && SpeechRecognizer.isRecognitionAvailable(this);
-    }
 
     // receive ringer mode changes to detect silent mode
     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@@ -2350,7 +1967,7 @@
         }
     }
 
-    private void vibrate() {
+    public void vibrate() {
         if (!mVibrateOn) {
             return;
         }
@@ -2453,24 +2070,13 @@
                 mResources.getBoolean(R.bool.default_popup_preview));
         mAutoCap = sp.getBoolean(PREF_AUTO_CAP, true);
         mQuickFixes = sp.getBoolean(PREF_QUICK_FIXES, true);
-        mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
-        mHasUsedVoiceInputUnsupportedLocale =
-                sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
-
-        mLocaleSupportedForVoiceInput = SubtypeSwitcher.getInstance().isVoiceSupported(
-                SubtypeSwitcher.getInstance().getInputLocaleStr());
 
         mAutoCorrectEnabled = isAutoCorrectEnabled(sp);
         mBigramSuggestionEnabled = mAutoCorrectEnabled && isBigramSuggestionEnabled(sp);
         loadAndSetAutoCompletionThreshold(sp);
 
-        if (VOICE_INSTALLED) {
-            final String voiceMode = sp.getString(PREF_VOICE_MODE,
-                    getString(R.string.voice_mode_main));
-            mVoiceKeyEnabled = !voiceMode.equals(getString(R.string.voice_mode_off))
-                    && shouldShowVoiceButton(makeFieldContext(), attribute);
-            mVoiceButtonOnPrimary = voiceMode.equals(getString(R.string.voice_mode_main));
-        }
+        mVoiceConnector.loadSettings(attribute, sp);
+
         updateCorrectionMode();
         updateAutoTextEnabled();
         updateSuggestionVisibility(sp);
diff --git a/java/src/com/android/inputmethod/latin/LatinIMESettings.java b/java/src/com/android/inputmethod/latin/LatinIMESettings.java
index 960c54d..8da9c22 100644
--- a/java/src/com/android/inputmethod/latin/LatinIMESettings.java
+++ b/java/src/com/android/inputmethod/latin/LatinIMESettings.java
@@ -33,6 +33,7 @@
 import android.text.AutoText;
 import android.util.Log;
 
+import com.android.inputmethod.voice.VoiceIMEConnector;
 import com.android.inputmethod.voice.VoiceInputLogger;
 
 public class LatinIMESettings extends PreferenceActivity
@@ -108,7 +109,7 @@
             ((PreferenceGroup) findPreference(PREDICTION_SETTINGS_KEY))
                     .removePreference(mQuickFixes);
         }
-        if (!LatinIME.VOICE_INSTALLED
+        if (!VoiceIMEConnector.VOICE_INSTALLED
                 || !SpeechRecognizer.isRecognitionAvailable(this)) {
             getPreferenceScreen().removePreference(mVoicePreference);
         } else {
diff --git a/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java b/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java
new file mode 100644
index 0000000..23b6752
--- /dev/null
+++ b/java/src/com/android/inputmethod/voice/VoiceIMEConnector.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.voice;
+
+import com.android.inputmethod.latin.EditingUtil;
+import com.android.inputmethod.latin.Hints;
+import com.android.inputmethod.latin.KeyboardSwitcher;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SharedPreferencesCompat;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.LatinIME.UIHandler;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.speech.SpeechRecognizer;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class VoiceIMEConnector implements VoiceInput.UiListener {
+    private static final VoiceIMEConnector sInstance = new VoiceIMEConnector();
+
+    public static final boolean VOICE_INSTALLED = true;
+    private static final boolean ENABLE_VOICE_BUTTON = true;
+    private static final String PREF_VOICE_MODE = "voice_mode";
+    // Whether or not the user has used voice input before (and thus, whether to show the
+    // first-run warning dialog or not).
+    private static final String PREF_HAS_USED_VOICE_INPUT = "has_used_voice_input";
+    // Whether or not the user has used voice input from an unsupported locale UI before.
+    // For example, the user has a Chinese UI but activates voice input.
+    private static final String PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE =
+            "has_used_voice_input_unsupported_locale";
+    // The private IME option used to indicate that no microphone should be shown for a
+    // given text field. For instance this is specified by the search dialog when the
+    // dialog is already showing a voice search button.
+    private static final String IME_OPTION_NO_MICROPHONE = "nm";
+
+    private boolean mAfterVoiceInput;
+    private boolean mHasUsedVoiceInput;
+    private boolean mHasUsedVoiceInputUnsupportedLocale;
+    private boolean mImmediatelyAfterVoiceInput;
+    private boolean mIsShowingHint;
+    private boolean mLocaleSupportedForVoiceInput;
+    private boolean mPasswordText;
+    private boolean mRecognizing;
+    private boolean mShowingVoiceSuggestions;
+    private boolean mVoiceButtonEnabled;
+    private boolean mVoiceButtonOnPrimary;
+    private boolean mVoiceInputHighlighted;
+
+    private LatinIME mContext;
+    private AlertDialog mVoiceWarningDialog;
+    private VoiceInput mVoiceInput;
+    private final VoiceResults mVoiceResults = new VoiceResults();
+    private Hints mHints;
+    private UIHandler mHandler;
+    // For each word, a list of potential replacements, usually from voice.
+    private final Map<String, List<CharSequence>> mWordToSuggestions =
+            new HashMap<String, List<CharSequence>>();
+
+    public static VoiceIMEConnector init(LatinIME context, UIHandler h) {
+        sInstance.initInternal(context, h);
+        return sInstance;
+    }
+
+    private void initInternal(LatinIME context, UIHandler h) {
+        mContext = context;
+        mHandler = h;
+        if (VOICE_INSTALLED) {
+            mVoiceInput = new VoiceInput(context, this);
+            mHints = new Hints(context, new Hints.Display() {
+                public void showHint(int viewResource) {
+                    LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                            Context.LAYOUT_INFLATER_SERVICE);
+                    View view = inflater.inflate(viewResource, null);
+                    mContext.setCandidatesView(view);
+                    mContext.setCandidatesViewShown(true);
+                    mIsShowingHint = true;
+                }
+              });
+        }
+    }
+
+    private VoiceIMEConnector() {
+    }
+
+    public void resetVoiceStates(boolean isPasswordText) {
+        mAfterVoiceInput = false;
+        mImmediatelyAfterVoiceInput = false;
+        mShowingVoiceSuggestions = false;
+        mVoiceInputHighlighted = false;
+        mPasswordText = isPasswordText;
+    }
+
+    public void flushVoiceInputLogs(boolean configurationChanged) {
+        if (VOICE_INSTALLED && !configurationChanged) {
+            if (mAfterVoiceInput) {
+                mVoiceInput.flushAllTextModificationCounters();
+                mVoiceInput.logInputEnded();
+            }
+            mVoiceInput.flushLogs();
+            mVoiceInput.cancel();
+        }
+    }
+
+    public void flushAndLogAllTextModificationCounters(int index, CharSequence suggestion,
+            String wordSeparators) {
+        if (mAfterVoiceInput && mShowingVoiceSuggestions) {
+            mVoiceInput.flushAllTextModificationCounters();
+            // send this intent AFTER logging any prior aggregated edits.
+            mVoiceInput.logTextModifiedByChooseSuggestion(suggestion.toString(), index,
+                    wordSeparators, mContext.getCurrentInputConnection());
+        }
+    }
+
+    private void showVoiceWarningDialog(final boolean swipe, IBinder token,
+            final boolean configurationChanging) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+        builder.setCancelable(true);
+        builder.setIcon(R.drawable.ic_mic_dialog);
+        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int whichButton) {
+                mVoiceInput.logKeyboardWarningDialogOk();
+                reallyStartListening(swipe, configurationChanging);
+            }
+        });
+        builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int whichButton) {
+                mVoiceInput.logKeyboardWarningDialogCancel();
+            }
+        });
+
+        if (mLocaleSupportedForVoiceInput) {
+            String message = mContext.getString(R.string.voice_warning_may_not_understand)
+                    + "\n\n" + mContext.getString(R.string.voice_warning_how_to_turn_off);
+            builder.setMessage(message);
+        } else {
+            String message = mContext.getString(R.string.voice_warning_locale_not_supported)
+                    + "\n\n" + mContext.getString(R.string.voice_warning_may_not_understand)
+                    + "\n\n" + mContext.getString(R.string.voice_warning_how_to_turn_off);
+            builder.setMessage(message);
+        }
+
+        builder.setTitle(R.string.voice_warning_title);
+        mVoiceWarningDialog = builder.create();
+        Window window = mVoiceWarningDialog.getWindow();
+        WindowManager.LayoutParams lp = window.getAttributes();
+        lp.token = token;
+        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+        window.setAttributes(lp);
+        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+        mVoiceInput.logKeyboardWarningDialogShown();
+        mVoiceWarningDialog.show();
+    }
+
+    public void showPunctuationHintIfNecessary() {
+        InputConnection ic = mContext.getCurrentInputConnection();
+        if (!mImmediatelyAfterVoiceInput && mAfterVoiceInput && ic != null) {
+            if (mHints.showPunctuationHintIfNecessary(ic)) {
+                mVoiceInput.logPunctuationHintDisplayed();
+            }
+        }
+        mImmediatelyAfterVoiceInput = false;
+    }
+
+    public void hideVoiceWindow(boolean configurationChanging) {
+        if (!configurationChanging) {
+            if (mAfterVoiceInput)
+                mVoiceInput.logInputEnded();
+            if (mVoiceWarningDialog != null && mVoiceWarningDialog.isShowing()) {
+                mVoiceInput.logKeyboardWarningDialogDismissed();
+                mVoiceWarningDialog.dismiss();
+                mVoiceWarningDialog = null;
+            }
+            if (VOICE_INSTALLED & mRecognizing) {
+                mVoiceInput.cancel();
+            }
+        }
+        mWordToSuggestions.clear();
+    }
+
+    public void setCursorAndSelection(int newSelEnd, int newSelStart) {
+        if (mAfterVoiceInput) {
+            mVoiceInput.setCursorPos(newSelEnd);
+            mVoiceInput.setSelectionSpan(newSelEnd - newSelStart);
+        }
+    }
+
+    public void setVoiceInputHighlighted(boolean b) {
+        mVoiceInputHighlighted = b;
+    }
+
+    public void setShowingVoiceSuggestions(boolean b) {
+        mShowingVoiceSuggestions = b;
+    }
+
+    public boolean isVoiceButtonEnabled() {
+        return mVoiceButtonEnabled;
+    }
+
+    public boolean isVoiceButtonOnPrimary() {
+        return mVoiceButtonOnPrimary;
+    }
+
+    public boolean isVoiceInputHighlighted() {
+        return mVoiceInputHighlighted;
+    }
+
+    public boolean isRecognizing() {
+        return mRecognizing;
+    }
+
+    public boolean getAndResetIsShowingHint() {
+        boolean ret = mIsShowingHint;
+        mIsShowingHint = false;
+        return ret;
+    }
+
+    private void revertVoiceInput() {
+        InputConnection ic = mContext.getCurrentInputConnection();
+        if (ic != null) ic.commitText("", 1);
+        mContext.updateSuggestions();
+        mVoiceInputHighlighted = false;
+    }
+
+    public void commitVoiceInput() {
+        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+            InputConnection ic = mContext.getCurrentInputConnection();
+            if (ic != null) ic.finishComposingText();
+            mContext.updateSuggestions();
+            mVoiceInputHighlighted = false;
+        }
+    }
+
+    public boolean logAndRevertVoiceInput() {
+        if (VOICE_INSTALLED && mVoiceInputHighlighted) {
+            mVoiceInput.incrementTextModificationDeleteCount(
+                    mVoiceResults.candidates.get(0).toString().length());
+            revertVoiceInput();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void rememberReplacedWord(CharSequence suggestion,String wordSeparators) {
+        if (mShowingVoiceSuggestions) {
+            // Retain the replaced word in the alternatives array.
+            EditingUtil.Range range = new EditingUtil.Range();
+            String wordToBeReplaced = EditingUtil.getWordAtCursor(
+                    mContext.getCurrentInputConnection(), wordSeparators, range);
+            if (!mWordToSuggestions.containsKey(wordToBeReplaced)) {
+                wordToBeReplaced = wordToBeReplaced.toLowerCase();
+            }
+            if (mWordToSuggestions.containsKey(wordToBeReplaced)) {
+                List<CharSequence> suggestions = mWordToSuggestions.get(wordToBeReplaced);
+                if (suggestions.contains(suggestion)) {
+                    suggestions.remove(suggestion);
+                }
+                suggestions.add(wordToBeReplaced);
+                mWordToSuggestions.remove(wordToBeReplaced);
+                mWordToSuggestions.put(suggestion.toString(), suggestions);
+            }
+        }
+    }
+
+    /**
+     * Tries to apply any voice alternatives for the word if this was a spoken word and
+     * there are voice alternatives.
+     * @param touching The word that the cursor is touching, with position information
+     * @return true if an alternative was found, false otherwise.
+     */
+    public boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) {
+        // Search for result in spoken word alternatives
+        String selectedWord = touching.word.toString().trim();
+        if (!mWordToSuggestions.containsKey(selectedWord)) {
+            selectedWord = selectedWord.toLowerCase();
+        }
+        if (mWordToSuggestions.containsKey(selectedWord)) {
+            mShowingVoiceSuggestions = true;
+            List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
+            // If the first letter of touching is capitalized, make all the suggestions
+            // start with a capital letter.
+            if (Character.isUpperCase(touching.word.charAt(0))) {
+                for (int i = 0; i < suggestions.size(); i++) {
+                    String origSugg = (String) suggestions.get(i);
+                    String capsSugg = origSugg.toUpperCase().charAt(0)
+                            + origSugg.subSequence(1, origSugg.length()).toString();
+                    suggestions.set(i, capsSugg);
+                }
+            }
+            mContext.setSuggestions(suggestions, false, true, true);
+            mContext.setCandidatesViewShown(true);
+            return true;
+        }
+        return false;
+    }
+
+    public void handleBackspace() {
+        if (mAfterVoiceInput) {
+            // Don't log delete if the user is pressing delete at
+            // the beginning of the text box (hence not deleting anything)
+            if (mVoiceInput.getCursorPos() > 0) {
+                // If anything was selected before the delete was pressed, increment the
+                // delete count by the length of the selection
+                int deleteLen  =  mVoiceInput.getSelectionSpan() > 0 ?
+                        mVoiceInput.getSelectionSpan() : 1;
+                mVoiceInput.incrementTextModificationDeleteCount(deleteLen);
+            }
+        }
+    }
+
+    public void handleCharacter() {
+        commitVoiceInput();
+        if (mAfterVoiceInput) {
+            // Assume input length is 1. This assumption fails for smiley face insertions.
+            mVoiceInput.incrementTextModificationInsertCount(1);
+        }
+    }
+
+    public void handleSeparator() {
+        commitVoiceInput();
+        if (mAfterVoiceInput){
+            // Assume input length is 1. This assumption fails for smiley face insertions.
+            mVoiceInput.incrementTextModificationInsertPunctuationCount(1);
+        }
+    }
+
+    public void handleClose() {
+        if (VOICE_INSTALLED & mRecognizing) {
+            mVoiceInput.cancel();
+        }
+    }
+
+
+    public void handleVoiceResults(KeyboardSwitcher switcher, boolean capitalizeFirstWord) {
+        mAfterVoiceInput = true;
+        mImmediatelyAfterVoiceInput = true;
+
+        InputConnection ic = mContext.getCurrentInputConnection();
+        if (!mContext.isFullscreenMode()) {
+            // Start listening for updates to the text from typing, etc.
+            if (ic != null) {
+                ExtractedTextRequest req = new ExtractedTextRequest();
+                ic.getExtractedText(req, InputConnection.GET_EXTRACTED_TEXT_MONITOR);
+            }
+        }
+        mContext.vibrate();
+        mContext.switchToKeyboardView();
+
+        final List<CharSequence> nBest = new ArrayList<CharSequence>();
+        for (String c : mVoiceResults.candidates) {
+            if (capitalizeFirstWord) {
+                c = Character.toUpperCase(c.charAt(0)) + c.substring(1, c.length());
+            }
+            nBest.add(c);
+        }
+        if (nBest.size() == 0) {
+            return;
+        }
+        String bestResult = nBest.get(0).toString();
+        mVoiceInput.logVoiceInputDelivered(bestResult.length());
+        mHints.registerVoiceResult(bestResult);
+
+        if (ic != null) ic.beginBatchEdit(); // To avoid extra updates on committing older text
+        mContext.commitTyped(ic);
+        EditingUtil.appendText(ic, bestResult);
+        if (ic != null) ic.endBatchEdit();
+
+        mVoiceInputHighlighted = true;
+        mWordToSuggestions.putAll(mVoiceResults.alternatives);
+    }
+
+    public void switchToRecognitionStatusView(final boolean configurationChanging) {
+        final boolean configChanged = configurationChanging;
+        mHandler.post(new Runnable() {
+            public void run() {
+                mContext.setCandidatesViewShown(false);
+                mRecognizing = true;
+                View v = mVoiceInput.getView();
+                ViewParent p = v.getParent();
+                if (p != null && p instanceof ViewGroup) {
+                    ((ViewGroup)v.getParent()).removeView(v);
+                }
+                mContext.setInputView(v);
+                mContext.updateInputViewShown();
+                if (configChanged) {
+                    mVoiceInput.onConfigurationChanged();
+                }
+        }});
+    }
+
+    private void reallyStartListening(boolean swipe, final boolean configurationChanging) {
+        if (!mHasUsedVoiceInput) {
+            // The user has started a voice input, so remember that in the
+            // future (so we don't show the warning dialog after the first run).
+            SharedPreferences.Editor editor =
+                    PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+            editor.putBoolean(PREF_HAS_USED_VOICE_INPUT, true);
+            SharedPreferencesCompat.apply(editor);
+            mHasUsedVoiceInput = true;
+        }
+
+        if (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale) {
+            // The user has started a voice input from an unsupported locale, so remember that
+            // in the future (so we don't show the warning dialog the next time they do this).
+            SharedPreferences.Editor editor =
+                    PreferenceManager.getDefaultSharedPreferences(mContext).edit();
+            editor.putBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, true);
+            SharedPreferencesCompat.apply(editor);
+            mHasUsedVoiceInputUnsupportedLocale = true;
+        }
+
+        // Clear N-best suggestions
+        mContext.clearSuggestions();
+
+        FieldContext context = makeFieldContext();
+        mVoiceInput.startListening(context, swipe);
+        switchToRecognitionStatusView(configurationChanging);
+    }
+
+    public void startListening(final boolean swipe, IBinder token,
+            final boolean configurationChanging) {
+        if (VOICE_INSTALLED) {
+            if (!mHasUsedVoiceInput ||
+                    (!mLocaleSupportedForVoiceInput && !mHasUsedVoiceInputUnsupportedLocale)) {
+                // Calls reallyStartListening if user clicks OK, does nothing if user clicks Cancel.
+                showVoiceWarningDialog(swipe, token, configurationChanging);
+            } else {
+                reallyStartListening(swipe, configurationChanging);
+            }
+        }
+    }
+
+
+    private boolean fieldCanDoVoice(FieldContext fieldContext) {
+        return !mPasswordText
+                && mVoiceInput != null
+                && !mVoiceInput.isBlacklistedField(fieldContext);
+    }
+
+    private boolean shouldShowVoiceButton(FieldContext fieldContext, EditorInfo attribute) {
+        return ENABLE_VOICE_BUTTON && fieldCanDoVoice(fieldContext)
+                && !(attribute != null
+                        && IME_OPTION_NO_MICROPHONE.equals(attribute.privateImeOptions))
+                && SpeechRecognizer.isRecognitionAvailable(mContext);
+    }
+
+    public void loadSettings(EditorInfo attribute, SharedPreferences sp) {
+        mHasUsedVoiceInput = sp.getBoolean(PREF_HAS_USED_VOICE_INPUT, false);
+        mHasUsedVoiceInputUnsupportedLocale =
+                sp.getBoolean(PREF_HAS_USED_VOICE_INPUT_UNSUPPORTED_LOCALE, false);
+
+        mLocaleSupportedForVoiceInput = SubtypeSwitcher.getInstance().isVoiceSupported(
+                SubtypeSwitcher.getInstance().getInputLocaleStr());
+
+        if (VOICE_INSTALLED) {
+            final String voiceMode = sp.getString(PREF_VOICE_MODE,
+                    mContext.getString(R.string.voice_mode_main));
+            mVoiceButtonEnabled = !voiceMode.equals(mContext.getString(R.string.voice_mode_off))
+                    && shouldShowVoiceButton(makeFieldContext(), attribute);
+            mVoiceButtonOnPrimary = voiceMode.equals(mContext.getString(R.string.voice_mode_main));
+        }
+    }
+
+    public void destroy() {
+        if (VOICE_INSTALLED && mVoiceInput != null) {
+            mVoiceInput.destroy();
+        }
+    }
+
+    public void onConfigurationChanged(boolean configurationChanging) {
+        if (mRecognizing) {
+            switchToRecognitionStatusView(configurationChanging);
+        }
+    }
+    @Override
+    public void onCancelVoice() {
+        if (mRecognizing) {
+            mRecognizing = false;
+            mContext.switchToKeyboardView();
+        }
+    }
+
+    @Override
+    public void onVoiceResults(List<String> candidates,
+            Map<String, List<CharSequence>> alternatives) {
+        if (!mRecognizing) {
+            return;
+        }
+        mVoiceResults.candidates = candidates;
+        mVoiceResults.alternatives = alternatives;
+        mHandler.updateVoiceResults();
+    }
+
+    public FieldContext makeFieldContext() {
+        SubtypeSwitcher switcher = SubtypeSwitcher.getInstance();
+        return new FieldContext(mContext.getCurrentInputConnection(),
+                mContext.getCurrentInputEditorInfo(), switcher.getInputLocaleStr(),
+                switcher.getEnabledLanguages());
+    }
+
+    private class VoiceResults {
+        List<String> candidates;
+        Map<String, List<CharSequence>> alternatives;
+    }
+}