InputLogic.java revision 07f7536b04142c00188bc52e96922904dd07e763
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin.inputlogic;
18
19import android.os.SystemClock;
20import android.text.TextUtils;
21import android.text.style.SuggestionSpan;
22import android.util.Log;
23import android.view.KeyCharacterMap;
24import android.view.KeyEvent;
25import android.view.inputmethod.CorrectionInfo;
26import android.view.inputmethod.EditorInfo;
27
28import com.android.inputmethod.compat.SuggestionSpanUtils;
29import com.android.inputmethod.event.EventInterpreter;
30import com.android.inputmethod.keyboard.Keyboard;
31import com.android.inputmethod.keyboard.KeyboardSwitcher;
32import com.android.inputmethod.keyboard.MainKeyboardView;
33import com.android.inputmethod.latin.Constants;
34import com.android.inputmethod.latin.Dictionary;
35import com.android.inputmethod.latin.InputPointers;
36import com.android.inputmethod.latin.LastComposedWord;
37import com.android.inputmethod.latin.LatinIME;
38import com.android.inputmethod.latin.LatinImeLogger;
39import com.android.inputmethod.latin.RichInputConnection;
40import com.android.inputmethod.latin.SubtypeSwitcher;
41import com.android.inputmethod.latin.Suggest;
42import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
43import com.android.inputmethod.latin.SuggestedWords;
44import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
45import com.android.inputmethod.latin.WordComposer;
46import com.android.inputmethod.latin.define.ProductionFlag;
47import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
48import com.android.inputmethod.latin.settings.Settings;
49import com.android.inputmethod.latin.settings.SettingsValues;
50import com.android.inputmethod.latin.suggestions.SuggestionStripView;
51import com.android.inputmethod.latin.utils.AsyncResultHolder;
52import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
53import com.android.inputmethod.latin.utils.CollectionUtils;
54import com.android.inputmethod.latin.utils.InputTypeUtils;
55import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
56import com.android.inputmethod.latin.utils.RecapitalizeStatus;
57import com.android.inputmethod.latin.utils.StringUtils;
58import com.android.inputmethod.latin.utils.TextRange;
59import com.android.inputmethod.research.ResearchLogger;
60
61import java.util.ArrayList;
62import java.util.TreeSet;
63import java.util.concurrent.TimeUnit;
64
65/**
66 * This class manages the input logic.
67 */
68public final class InputLogic {
69    private static final String TAG = InputLogic.class.getSimpleName();
70
71    // TODO : Remove this member when we can.
72    private final LatinIME mLatinIME;
73
74    // TODO : make all these fields private as soon as possible.
75    // Current space state of the input method. This can be any of the above constants.
76    public int mSpaceState;
77    // Never null
78    public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
79    public Suggest mSuggest;
80    // The event interpreter should never be null.
81    public EventInterpreter mEventInterpreter;
82
83    public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
84    public final WordComposer mWordComposer;
85    public final RichInputConnection mConnection;
86    public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
87
88    // Keep track of the last selection range to decide if we need to show word alternatives
89    public int mLastSelectionStart = Constants.NOT_A_CURSOR_POSITION;
90    public int mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
91
92    public int mDeleteCount;
93    private long mLastKeyTime;
94    public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
95
96    // Keeps track of most recently inserted text (multi-character key) for reverting
97    public String mEnteredText;
98
99    // TODO: This boolean is persistent state and causes large side effects at unexpected times.
100    // Find a way to remove it for readability.
101    public boolean mIsAutoCorrectionIndicatorOn;
102
103    public InputLogic(final LatinIME latinIME) {
104        mLatinIME = latinIME;
105        mWordComposer = new WordComposer();
106        mEventInterpreter = new EventInterpreter(latinIME);
107        mConnection = new RichInputConnection(latinIME);
108    }
109
110    /**
111     * Initializes the input logic for input in an editor.
112     *
113     * Call this when input starts or restarts in some editor (typically, in onStartInputView).
114     * If the input is starting in the same field as before, set `restarting' to true. This allows
115     * the input logic to reset only necessary stuff and save performance. Also, when restarting
116     * some things must not be done (for example, the keyboard should not be reset to the
117     * alphabetic layout), so do not send false to this just in case.
118     *
119     * @param restarting whether input is starting in the same field as before.
120     */
121    public void startInput(final boolean restarting) {
122    }
123
124    /**
125     * Clean up the input logic after input is finished.
126     */
127    public void finishInput() {
128    }
129
130    /**
131     * React to a string input.
132     *
133     * This is triggered by keys that input many characters at once, like the ".com" key or
134     * some additional keys for example.
135     *
136     * @param settingsValues the current values of the settings.
137     * @param rawText the text to input.
138     */
139    public void onTextInput(final SettingsValues settingsValues, final String rawText,
140            // TODO: remove these arguments
141            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
142        mConnection.beginBatchEdit();
143        if (mWordComposer.isComposingWord()) {
144            commitCurrentAutoCorrection(settingsValues, rawText, handler);
145        } else {
146            resetComposingState(true /* alsoResetLastComposedWord */);
147        }
148        handler.postUpdateSuggestionStrip();
149        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
150                && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) {
151            ResearchLogger.getInstance().onResearchKeySelected(mLatinIME);
152            return;
153        }
154        final String text = performSpecificTldProcessingOnTextInput(rawText);
155        if (SpaceState.PHANTOM == mSpaceState) {
156            promotePhantomSpace(settingsValues);
157        }
158        mConnection.commitText(text, 1);
159        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
160            ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
161        }
162        mConnection.endBatchEdit();
163        // Space state must be updated before calling updateShiftState
164        mSpaceState = SpaceState.NONE;
165        keyboardSwitcher.updateShiftState();
166        keyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
167        mEnteredText = text;
168    }
169
170    /**
171     * React to a code input. It may be a code point to insert, or a symbolic value that influences
172     * the keyboard behavior.
173     *
174     * Typically, this is called whenever a key is pressed on the software keyboard. This is not
175     * the entry point for gesture input; see the onBatchInput* family of functions for this.
176     *
177     * @param code the code to handle. It may be a code point, or an internal key code.
178     * @param x the x-coordinate where the user pressed the key, or NOT_A_COORDINATE.
179     * @param y the y-coordinate where the user pressed the key, or NOT_A_COORDINATE.
180     */
181    public void onCodeInput(final int code, final int x, final int y,
182            // TODO: remove these three arguments
183            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher,
184            final SubtypeSwitcher subtypeSwitcher) {
185        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
186            ResearchLogger.latinIME_onCodeInput(code, x, y);
187        }
188        final SettingsValues settingsValues = Settings.getInstance().getCurrent();
189        final long when = SystemClock.uptimeMillis();
190        if (code != Constants.CODE_DELETE
191                || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
192            mDeleteCount = 0;
193        }
194        mLastKeyTime = when;
195        mConnection.beginBatchEdit();
196        // The space state depends only on the last character pressed and its own previous
197        // state. Here, we revert the space state to neutral if the key is actually modifying
198        // the input contents (any non-shift key), which is what we should do for
199        // all inputs that do not result in a special state. Each character handling is then
200        // free to override the state as they see fit.
201        final int spaceState = mSpaceState;
202        if (!mWordComposer.isComposingWord()) {
203            mIsAutoCorrectionIndicatorOn = false;
204        }
205
206        // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
207        if (code != Constants.CODE_SPACE) {
208            handler.cancelDoubleSpacePeriodTimer();
209        }
210
211        boolean didAutoCorrect = false;
212        switch (code) {
213        case Constants.CODE_DELETE:
214            handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher);
215            LatinImeLogger.logOnDelete(x, y);
216            break;
217        case Constants.CODE_SHIFT:
218            // Note: Calling back to the keyboard on Shift key is handled in
219            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
220            final Keyboard currentKeyboard = keyboardSwitcher.getKeyboard();
221            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
222                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
223                // alphabetic shift and shift while in symbol layout.
224                performRecapitalization(settingsValues, keyboardSwitcher);
225            }
226            break;
227        case Constants.CODE_CAPSLOCK:
228            // Note: Changing keyboard to shift lock state is handled in
229            // {@link KeyboardSwitcher#onCodeInput(int)}.
230            break;
231        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
232            // Note: Calling back to the keyboard on symbol key is handled in
233            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
234            break;
235        case Constants.CODE_SETTINGS:
236            onSettingsKeyPressed();
237            break;
238        case Constants.CODE_SHORTCUT:
239            subtypeSwitcher.switchToShortcutIME(mLatinIME);
240            break;
241        case Constants.CODE_ACTION_NEXT:
242            performEditorAction(EditorInfo.IME_ACTION_NEXT);
243            break;
244        case Constants.CODE_ACTION_PREVIOUS:
245            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
246            break;
247        case Constants.CODE_LANGUAGE_SWITCH:
248            handleLanguageSwitchKey();
249            break;
250        case Constants.CODE_EMOJI:
251            // Note: Switching emoji keyboard is being handled in
252            // {@link KeyboardState#onCodeInput(int,int)}.
253            break;
254        case Constants.CODE_ENTER:
255            final EditorInfo editorInfo = getCurrentInputEditorInfo();
256            final int imeOptionsActionId =
257                    InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
258            if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
259                // Either we have an actionLabel and we should performEditorAction with actionId
260                // regardless of its value.
261                performEditorAction(editorInfo.actionId);
262            } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
263                // We didn't have an actionLabel, but we had another action to execute.
264                // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
265                // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
266                // means there should be an action and the app didn't bother to set a specific
267                // code for it - presumably it only handles one. It does not have to be treated
268                // in any specific way: anything that is not IME_ACTION_NONE should be sent to
269                // performEditorAction.
270                performEditorAction(imeOptionsActionId);
271            } else {
272                // No action label, and the action from imeOptions is NONE: this is a regular
273                // enter key that should input a carriage return.
274                didAutoCorrect = handleNonSpecialCharacter(settingsValues,
275                        Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
276            }
277            break;
278        case Constants.CODE_SHIFT_ENTER:
279            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
280                    Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
281            break;
282        default:
283            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
284                    code, x, y, spaceState, keyboardSwitcher, handler);
285            break;
286        }
287        keyboardSwitcher.onCodeInput(code);
288        // Reset after any single keystroke, except shift, capslock, and symbol-shift
289        if (!didAutoCorrect && code != Constants.CODE_SHIFT
290                && code != Constants.CODE_CAPSLOCK
291                && code != Constants.CODE_SWITCH_ALPHA_SYMBOL)
292            mLastComposedWord.deactivate();
293        if (Constants.CODE_DELETE != code) {
294            mEnteredText = null;
295        }
296        mConnection.endBatchEdit();
297    }
298
299    public void onStartBatchInput(final SettingsValues settingsValues,
300            // TODO: remove these arguments
301            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler,
302            final LatinIME.InputUpdater inputUpdater) {
303        inputUpdater.onStartBatchInput();
304        handler.cancelUpdateSuggestionStrip();
305        mConnection.beginBatchEdit();
306        if (mWordComposer.isComposingWord()) {
307            if (settingsValues.mIsInternal) {
308                if (mWordComposer.isBatchMode()) {
309                    LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
310                            mWordComposer);
311                }
312            }
313            final int wordComposerSize = mWordComposer.size();
314            // Since isComposingWord() is true, the size is at least 1.
315            if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
316                // If we are in the middle of a recorrection, we need to commit the recorrection
317                // first so that we can insert the batch input at the current cursor position.
318                resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
319            } else if (wordComposerSize <= 1) {
320                // We auto-correct the previous (typed, not gestured) string iff it's one character
321                // long. The reason for this is, even in the middle of gesture typing, you'll still
322                // tap one-letter words and you want them auto-corrected (typically, "i" in English
323                // should become "I"). However for any longer word, we assume that the reason for
324                // tapping probably is that the word you intend to type is not in the dictionary,
325                // so we do not attempt to correct, on the assumption that if that was a dictionary
326                // word, the user would probably have gestured instead.
327                commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR,
328                        handler);
329            } else {
330                commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
331            }
332        }
333        final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
334        if (Character.isLetterOrDigit(codePointBeforeCursor)
335                || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
336            final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
337                    getCurrentAutoCapsState(settingsValues);
338            mSpaceState = SpaceState.PHANTOM;
339            if (!autoShiftHasBeenOverriden) {
340                // When we change the space state, we need to update the shift state of the
341                // keyboard unless it has been overridden manually. This is happening for example
342                // after typing some letters and a period, then gesturing; the keyboard is not in
343                // caps mode yet, but since a gesture is starting, it should go in caps mode,
344                // unless the user explictly said it should not.
345                keyboardSwitcher.updateShiftState();
346            }
347        }
348        mConnection.endBatchEdit();
349        mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
350                getActualCapsMode(settingsValues, keyboardSwitcher),
351                // Prev word is 1st word before cursor
352                getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
353    }
354
355    /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
356     * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
357     * input pointers that are held in a singleton, and to know how much to trim we rely on the
358     * results of the suggestion process that is held in mSuggestedWords.
359     * However, the suggestion process is asynchronous, and sometimes we may enter the
360     * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
361     * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
362     * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
363     * remove an unrelated number of pointers (possibly even more than are left in the input
364     * pointers, leading to a crash).
365     * To avoid that, we increase the sequence number each time we auto-commit and trim the
366     * input pointers, and we do not use any suggested words that have been generated with an
367     * earlier sequence number.
368     */
369    private int mAutoCommitSequenceNumber = 1;
370    public void onUpdateBatchInput(final SettingsValues settingsValues,
371            final InputPointers batchPointers,
372            // TODO: remove these arguments
373            final KeyboardSwitcher keyboardSwitcher, final LatinIME.InputUpdater inputUpdater) {
374        if (settingsValues.mPhraseGestureEnabled) {
375            final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
376            // If these suggested words have been generated with out of date input pointers, then
377            // we skip auto-commit (see comments above on the mSequenceNumber member).
378            if (null != candidate
379                    && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
380                if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
381                    final String[] commitParts = candidate.mWord.split(" ", 2);
382                    batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
383                    promotePhantomSpace(settingsValues);
384                    mConnection.commitText(commitParts[0], 0);
385                    mSpaceState = SpaceState.PHANTOM;
386                    keyboardSwitcher.updateShiftState();
387                    mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
388                            getActualCapsMode(settingsValues, keyboardSwitcher), commitParts[0]);
389                    ++mAutoCommitSequenceNumber;
390                }
391            }
392        }
393        inputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
394    }
395
396    public void onEndBatchInput(final SettingsValues settingValues,
397            final InputPointers batchPointers,
398            // TODO: remove these arguments
399            final LatinIME.InputUpdater inputUpdater) {
400        inputUpdater.onEndBatchInput(batchPointers);
401    }
402
403    // TODO: remove this argument
404    public void onCancelBatchInput(final LatinIME.InputUpdater inputUpdater) {
405        inputUpdater.onCancelBatchInput();
406    }
407
408    /**
409     * Handle inputting a code point to the editor.
410     *
411     * Non-special keys are those that generate a single code point.
412     * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
413     * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
414     * any key that results in multiple code points like the ".com" key.
415     *
416     * @param settingsValues The current settings values.
417     * @param codePoint the code point associated with the key.
418     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
419     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
420     * @param spaceState the space state at start of the batch input.
421     * @return whether this caused an auto-correction to happen.
422     */
423    private boolean handleNonSpecialCharacter(final SettingsValues settingsValues,
424            final int codePoint, final int x, final int y, final int spaceState,
425            // TODO: remove these arguments
426            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
427        mSpaceState = SpaceState.NONE;
428        final boolean didAutoCorrect;
429        if (settingsValues.isWordSeparator(codePoint)
430                || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
431            didAutoCorrect = handleSeparator(settingsValues, codePoint, x, y, spaceState,
432                    keyboardSwitcher, handler);
433        } else {
434            didAutoCorrect = false;
435            if (SpaceState.PHANTOM == spaceState) {
436                if (settingsValues.mIsInternal) {
437                    if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
438                        LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
439                                mWordComposer);
440                    }
441                }
442                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
443                    // If we are in the middle of a recorrection, we need to commit the recorrection
444                    // first so that we can insert the character at the current cursor position.
445                    resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
446                } else {
447                    commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
448                }
449            }
450            final int keyX, keyY;
451            final Keyboard keyboard = keyboardSwitcher.getKeyboard();
452            if (keyboard != null && keyboard.hasProximityCharsCorrection(codePoint)) {
453                keyX = x;
454                keyY = y;
455            } else {
456                keyX = Constants.NOT_A_COORDINATE;
457                keyY = Constants.NOT_A_COORDINATE;
458            }
459            handleNonSeparator(settingsValues, codePoint, keyX, keyY, spaceState,
460                    keyboardSwitcher, handler);
461        }
462        return didAutoCorrect;
463    }
464
465    /**
466     * Handle a non-separator.
467     * @param settingsValues The current settings values.
468     * @param codePoint the code point associated with the key.
469     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
470     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
471     * @param spaceState the space state at start of the batch input.
472     */
473    private void handleNonSeparator(final SettingsValues settingsValues,
474            final int codePoint, final int x, final int y, final int spaceState,
475            // TODO: Remove these arguments
476            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
477        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
478        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
479        // which has the same name as other handle* methods but is not the same.
480        boolean isComposingWord = mWordComposer.isComposingWord();
481
482        // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
483        // See onStartBatchInput() to see how to do it.
484        if (SpaceState.PHANTOM == spaceState && !settingsValues.isWordConnector(codePoint)) {
485            if (isComposingWord) {
486                // Sanity check
487                throw new RuntimeException("Should not be composing here");
488            }
489            promotePhantomSpace(settingsValues);
490        }
491
492        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
493            // If we are in the middle of a recorrection, we need to commit the recorrection
494            // first so that we can insert the character at the current cursor position.
495            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
496            isComposingWord = false;
497        }
498        // We want to find out whether to start composing a new word with this character. If so,
499        // we need to reset the composing state and switch isComposingWord. The order of the
500        // tests is important for good performance.
501        // We only start composing if we're not already composing.
502        if (!isComposingWord
503        // We only start composing if this is a word code point. Essentially that means it's a
504        // a letter or a word connector.
505                && settingsValues.isWordCodePoint(codePoint)
506        // We never go into composing state if suggestions are not requested.
507                && settingsValues.isSuggestionsRequested() &&
508        // In languages with spaces, we only start composing a word when we are not already
509        // touching a word. In languages without spaces, the above conditions are sufficient.
510                (!mConnection.isCursorTouchingWord(settingsValues)
511                        || !settingsValues.mCurrentLanguageHasSpaces)) {
512            // Reset entirely the composing state anyway, then start composing a new word unless
513            // the character is a single quote or a dash. The idea here is, single quote and dash
514            // are not separators and they should be treated as normal characters, except in the
515            // first position where they should not start composing a word.
516            isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint
517                    && Constants.CODE_DASH != codePoint);
518            // Here we don't need to reset the last composed word. It will be reset
519            // when we commit this one, if we ever do; if on the other hand we backspace
520            // it entirely and resume suggestions on the previous word, we'd like to still
521            // have touch coordinates for it.
522            resetComposingState(false /* alsoResetLastComposedWord */);
523        }
524        if (isComposingWord) {
525            final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView();
526            // TODO: We should reconsider which coordinate system should be used to represent
527            // keyboard event.
528            final int keyX = mainKeyboardView.getKeyX(x);
529            final int keyY = mainKeyboardView.getKeyY(y);
530            mWordComposer.add(codePoint, keyX, keyY);
531            // If it's the first letter, make note of auto-caps state
532            if (mWordComposer.size() == 1) {
533                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
534                // yet, so the word we want is the 1st word before the cursor.
535                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
536                        getActualCapsMode(settingsValues, keyboardSwitcher),
537                        getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
538            }
539            mConnection.setComposingText(getTextWithUnderline(
540                    mWordComposer.getTypedWord()), 1);
541        } else {
542            final boolean swapWeakSpace = maybeStripSpace(settingsValues,
543                    codePoint, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
544
545            sendKeyCodePoint(settingsValues, codePoint);
546
547            if (swapWeakSpace) {
548                swapSwapperAndSpace(keyboardSwitcher);
549                mSpaceState = SpaceState.WEAK;
550            }
551            // In case the "add to dictionary" hint was still displayed.
552            mLatinIME.dismissAddToDictionaryHint();
553        }
554        handler.postUpdateSuggestionStrip();
555        if (settingsValues.mIsInternal) {
556            LatinImeLoggerUtils.onNonSeparator((char)codePoint, x, y);
557        }
558    }
559
560    /**
561     * Handle input of a separator code point.
562     * @param settingsValues The current settings values.
563     * @param codePoint the code point associated with the key.
564     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
565     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
566     * @param spaceState the space state at start of the batch input.
567     * @return whether this caused an auto-correction to happen.
568     */
569    private boolean handleSeparator(final SettingsValues settingsValues,
570            final int codePoint, final int x, final int y, final int spaceState,
571            // TODO: remove these arguments
572            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
573        boolean didAutoCorrect = false;
574        // We avoid sending spaces in languages without spaces if we were composing.
575        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
576                && !settingsValues.mCurrentLanguageHasSpaces
577                && mWordComposer.isComposingWord();
578        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
579            // If we are in the middle of a recorrection, we need to commit the recorrection
580            // first so that we can insert the separator at the current cursor position.
581            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
582        }
583        // isComposingWord() may have changed since we stored wasComposing
584        if (mWordComposer.isComposingWord()) {
585            if (settingsValues.mCorrectionEnabled) {
586                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
587                        : StringUtils.newSingleCodePointString(codePoint);
588                commitCurrentAutoCorrection(settingsValues, separator, handler);
589                didAutoCorrect = true;
590            } else {
591                commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint));
592            }
593        }
594
595        final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
596                Constants.SUGGESTION_STRIP_COORDINATE == x);
597
598        if (SpaceState.PHANTOM == spaceState &&
599                settingsValues.isUsuallyPrecededBySpace(codePoint)) {
600            promotePhantomSpace(settingsValues);
601        }
602        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
603            ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord());
604        }
605
606        if (!shouldAvoidSendingCode) {
607            sendKeyCodePoint(settingsValues, codePoint);
608        }
609
610        if (Constants.CODE_SPACE == codePoint) {
611            if (settingsValues.isSuggestionsRequested()) {
612                if (maybeDoubleSpacePeriod(settingsValues, keyboardSwitcher, handler)) {
613                    mSpaceState = SpaceState.DOUBLE;
614                } else if (!mLatinIME.isShowingPunctuationList()) {
615                    mSpaceState = SpaceState.WEAK;
616                }
617            }
618
619            handler.startDoubleSpacePeriodTimer();
620            handler.postUpdateSuggestionStrip();
621        } else {
622            if (swapWeakSpace) {
623                swapSwapperAndSpace(keyboardSwitcher);
624                mSpaceState = SpaceState.SWAP_PUNCTUATION;
625            } else if (SpaceState.PHANTOM == spaceState
626                    && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
627                // If we are in phantom space state, and the user presses a separator, we want to
628                // stay in phantom space state so that the next keypress has a chance to add the
629                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
630                // then insert a comma and go on to typing the next word, I want the space to be
631                // inserted automatically before the next word, the same way it is when I don't
632                // input the comma.
633                // The case is a little different if the separator is a space stripper. Such a
634                // separator does not normally need a space on the right (that's the difference
635                // between swappers and strippers), so we should not stay in phantom space state if
636                // the separator is a stripper. Hence the additional test above.
637                mSpaceState = SpaceState.PHANTOM;
638            }
639
640            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
641            // already displayed or not, so it's okay.
642            mLatinIME.setPunctuationSuggestions();
643        }
644        if (settingsValues.mIsInternal) {
645            LatinImeLoggerUtils.onSeparator((char)codePoint, x, y);
646        }
647
648        keyboardSwitcher.updateShiftState();
649        return didAutoCorrect;
650    }
651
652    /**
653     * Handle a press on the backspace key.
654     * @param settingsValues The current settings values.
655     * @param spaceState The space state at start of this batch edit.
656     */
657    private void handleBackspace(final SettingsValues settingsValues, final int spaceState,
658            // TODO: remove these arguments
659            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
660        mSpaceState = SpaceState.NONE;
661        mDeleteCount++;
662
663        // In many cases, we may have to put the keyboard in auto-shift state again. However
664        // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
665        // during key repeat.
666        handler.postUpdateShiftState();
667
668        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
669            // If we are in the middle of a recorrection, we need to commit the recorrection
670            // first so that we can remove the character at the current cursor position.
671            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
672            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
673        }
674        if (mWordComposer.isComposingWord()) {
675            if (mWordComposer.isBatchMode()) {
676                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
677                    final String word = mWordComposer.getTypedWord();
678                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
679                }
680                final String rejectedSuggestion = mWordComposer.getTypedWord();
681                mWordComposer.reset();
682                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
683            } else {
684                mWordComposer.deleteLast();
685            }
686            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
687            handler.postUpdateSuggestionStrip();
688            if (!mWordComposer.isComposingWord()) {
689                // If we just removed the last character, auto-caps mode may have changed so we
690                // need to re-evaluate.
691                keyboardSwitcher.updateShiftState();
692            }
693        } else {
694            if (mLastComposedWord.canRevertCommit()) {
695                if (settingsValues.mIsInternal) {
696                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
697                }
698                revertCommit(settingsValues, keyboardSwitcher, handler);
699                return;
700            }
701            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
702                // Cancel multi-character input: remove the text we just entered.
703                // This is triggered on backspace after a key that inputs multiple characters,
704                // like the smiley key or the .com key.
705                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
706                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
707                    ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
708                }
709                mEnteredText = null;
710                // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
711                // In addition we know that spaceState is false, and that we should not be
712                // reverting any autocorrect at this point. So we can safely return.
713                return;
714            }
715            if (SpaceState.DOUBLE == spaceState) {
716                handler.cancelDoubleSpacePeriodTimer();
717                if (mConnection.revertDoubleSpacePeriod()) {
718                    // No need to reset mSpaceState, it has already be done (that's why we
719                    // receive it as a parameter)
720                    return;
721                }
722            } else if (SpaceState.SWAP_PUNCTUATION == spaceState) {
723                if (mConnection.revertSwapPunctuation()) {
724                    // Likewise
725                    return;
726                }
727            }
728
729            // No cancelling of commit/double space/swap: we have a regular backspace.
730            // We should backspace one char and restart suggestion if at the end of a word.
731            if (mLastSelectionStart != mLastSelectionEnd) {
732                // If there is a selection, remove it.
733                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
734                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
735                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
736                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
737                // but we want to set it right away to avoid it being used with the wrong values
738                // later (typically, in a subsequent press on backspace).
739                mLastSelectionEnd = mLastSelectionStart;
740                mConnection.deleteSurroundingText(numCharsDeleted, 0);
741                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
742                    ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
743                            false /* shouldUncommitLogUnit */);
744                }
745            } else {
746                // There is no selection, just delete one character.
747                if (Constants.NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
748                    // This should never happen.
749                    Log.e(TAG, "Backspace when we don't know the selection position");
750                }
751                if (settingsValues.isBeforeJellyBean() ||
752                        settingsValues.mInputAttributes.isTypeNull()) {
753                    // There are two possible reasons to send a key event: either the field has
754                    // type TYPE_NULL, in which case the keyboard should send events, or we are
755                    // running in backward compatibility mode. Before Jelly bean, the keyboard
756                    // would simulate a hardware keyboard event on pressing enter or delete. This
757                    // is bad for many reasons (there are race conditions with commits) but some
758                    // applications are relying on this behavior so we continue to support it for
759                    // older apps, so we retain this behavior if the app has target SDK < JellyBean.
760                    sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
761                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
762                        sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
763                    }
764                } else {
765                    final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
766                    if (codePointBeforeCursor == Constants.NOT_A_CODE) {
767                        // Nothing to delete before the cursor.
768                        return;
769                    }
770                    final int lengthToDelete =
771                            Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
772                    mConnection.deleteSurroundingText(lengthToDelete, 0);
773                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
774                        ResearchLogger.latinIME_handleBackspace(lengthToDelete,
775                                true /* shouldUncommitLogUnit */);
776                    }
777                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
778                        final int codePointBeforeCursorToDeleteAgain =
779                                mConnection.getCodePointBeforeCursor();
780                        if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
781                            final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
782                                    codePointBeforeCursorToDeleteAgain) ? 2 : 1;
783                            mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
784                            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
785                                ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
786                                        true /* shouldUncommitLogUnit */);
787                            }
788                        }
789                    }
790                }
791            }
792            if (settingsValues.isSuggestionsRequested()
793                    && settingsValues.mCurrentLanguageHasSpaces) {
794                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(settingsValues, keyboardSwitcher,
795                        handler);
796            }
797            // We just removed a character. We need to update the auto-caps state.
798            keyboardSwitcher.updateShiftState();
799        }
800    }
801
802    /**
803     * Handle a press on the language switch key (the "globe key")
804     */
805    private void handleLanguageSwitchKey() {
806        mLatinIME.switchToNextSubtype();
807    }
808
809    /**
810     * Swap a space with a space-swapping punctuation sign.
811     *
812     * This method will check that there are two characters before the cursor and that the first
813     * one is a space before it does the actual swapping.
814     */
815    // TODO: Remove this argument
816    private void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) {
817        final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
818        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
819        if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
820            mConnection.deleteSurroundingText(2, 0);
821            final String text = lastTwo.charAt(1) + " ";
822            mConnection.commitText(text, 1);
823            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
824                ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
825            }
826            keyboardSwitcher.updateShiftState();
827        }
828    }
829
830    /*
831     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
832     * @param settingsValues The current settings values.
833     * @param codePoint The code point that is about to be inserted.
834     * @param spaceState The space state at start of this batch edit.
835     * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip.
836     * @return whether we should swap the space instead of removing it.
837     */
838    private boolean maybeStripSpace(final SettingsValues settingsValues,
839            final int code, final int spaceState, final boolean isFromSuggestionStrip) {
840        if (Constants.CODE_ENTER == code && SpaceState.SWAP_PUNCTUATION == spaceState) {
841            mConnection.removeTrailingSpace();
842            return false;
843        }
844        if ((SpaceState.WEAK == spaceState || SpaceState.SWAP_PUNCTUATION == spaceState)
845                && isFromSuggestionStrip) {
846            if (settingsValues.isUsuallyPrecededBySpace(code)) return false;
847            if (settingsValues.isUsuallyFollowedBySpace(code)) return true;
848            mConnection.removeTrailingSpace();
849        }
850        return false;
851    }
852
853    /**
854     * Apply the double-space-to-period transformation if applicable.
855     *
856     * The double-space-to-period transformation means that we replace two spaces with a
857     * period-space sequence of characters. This typically happens when the user presses space
858     * twice in a row quickly.
859     * This method will check that the double-space-to-period is active in settings, that the
860     * two spaces have been input close enough together, and that the previous character allows
861     * for the transformation to take place. If all of these conditions are fulfilled, this
862     * method applies the transformation and returns true. Otherwise, it does nothing and
863     * returns false.
864     *
865     * @param settingsValues the current values of the settings.
866     * @return true if we applied the double-space-to-period transformation, false otherwise.
867     */
868    private boolean maybeDoubleSpacePeriod(final SettingsValues settingsValues,
869            // TODO: remove these arguments
870            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
871        if (!settingsValues.mUseDoubleSpacePeriod) return false;
872        if (!handler.isAcceptingDoubleSpacePeriod()) return false;
873        // We only do this when we see two spaces and an accepted code point before the cursor.
874        // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
875        final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
876        if (null == lastThree) return false;
877        final int length = lastThree.length();
878        if (length < 3) return false;
879        if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
880        if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
881        // We know there are spaces in pos -1 and -2, and we have at least three chars.
882        // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
883        // so this is fine.
884        final int firstCodePoint =
885                Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
886                        Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
887        if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
888            handler.cancelDoubleSpacePeriodTimer();
889            mConnection.deleteSurroundingText(2, 0);
890            final String textToInsert = new String(
891                    new int[] { settingsValues.mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
892            mConnection.commitText(textToInsert, 1);
893            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
894                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
895                        false /* isBatchMode */);
896            }
897            mWordComposer.discardPreviousWordForSuggestion();
898            keyboardSwitcher.updateShiftState();
899            return true;
900        }
901        return false;
902    }
903
904    /**
905     * Returns whether this code point can be followed by the double-space-to-period transformation.
906     *
907     * See #maybeDoubleSpaceToPeriod for details.
908     * Generally, most word characters can be followed by the double-space-to-period transformation,
909     * while most punctuation can't. Some punctuation however does allow for this to take place
910     * after them, like the closing parenthesis for example.
911     *
912     * @param codePoint the code point after which we may want to apply the transformation
913     * @return whether it's fine to apply the transformation after this code point.
914     */
915    private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
916        // TODO: This should probably be a blacklist rather than a whitelist.
917        // TODO: This should probably be language-dependant...
918        return Character.isLetterOrDigit(codePoint)
919                || codePoint == Constants.CODE_SINGLE_QUOTE
920                || codePoint == Constants.CODE_DOUBLE_QUOTE
921                || codePoint == Constants.CODE_CLOSING_PARENTHESIS
922                || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
923                || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
924                || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
925                || codePoint == Constants.CODE_PLUS
926                || codePoint == Constants.CODE_PERCENT
927                || Character.getType(codePoint) == Character.OTHER_SYMBOL;
928    }
929
930    /**
931     * Performs a recapitalization event.
932     * @param settingsValues The current settings values.
933     */
934    private void performRecapitalization(final SettingsValues settingsValues,
935            // TODO: remove this argument.
936            final KeyboardSwitcher keyboardSwitcher) {
937        if (mLastSelectionStart == mLastSelectionEnd) {
938            return; // No selection
939        }
940        // If we have a recapitalize in progress, use it; otherwise, create a new one.
941        if (!mRecapitalizeStatus.isActive()
942                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
943            final CharSequence selectedText =
944                    mConnection.getSelectedText(0 /* flags, 0 for no styles */);
945            if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
946            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
947                    selectedText.toString(),
948                    settingsValues.mLocale, settingsValues.mWordSeparators);
949            // We trim leading and trailing whitespace.
950            mRecapitalizeStatus.trim();
951            // Trimming the object may have changed the length of the string, and we need to
952            // reposition the selection handles accordingly. As this result in an IPC call,
953            // only do it if it's actually necessary, in other words if the recapitalize status
954            // is not set at the same place as before.
955            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
956                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
957                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
958            }
959        }
960        mConnection.finishComposingText();
961        mRecapitalizeStatus.rotate();
962        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
963        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
964        mConnection.deleteSurroundingText(numCharsDeleted, 0);
965        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
966        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
967        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
968        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
969        // Match the keyboard to the new state.
970        keyboardSwitcher.updateShiftState();
971    }
972
973    private String performAdditionToUserHistoryDictionary(final SettingsValues settingsValues,
974            final String suggestion) {
975        // If correction is not enabled, we don't add words to the user history dictionary.
976        // That's to avoid unintended additions in some sensitive fields, or fields that
977        // expect to receive non-words.
978        if (!settingsValues.mCorrectionEnabled) return null;
979
980        if (TextUtils.isEmpty(suggestion)) return null;
981        final Suggest suggest = mSuggest;
982        if (suggest == null) return null;
983
984        final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary();
985        if (userHistoryDictionary == null) return null;
986
987        final String prevWord = mConnection.getNthPreviousWord(settingsValues, 2);
988        final String secondWord;
989        if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
990            secondWord = suggestion.toLowerCase(settingsValues.mLocale);
991        } else {
992            secondWord = suggestion;
993        }
994        // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
995        // We don't add words with 0-frequency (assuming they would be profanity etc.).
996        final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
997                suggest.getUnigramDictionaries(), suggestion);
998        if (maxFreq == 0) return null;
999        userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0,
1000                (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis())));
1001        return prevWord;
1002    }
1003
1004    public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
1005            // TODO: Remove this variable
1006            final LatinIME.UIHandler handler) {
1007        handler.cancelUpdateSuggestionStrip();
1008
1009        // Check if we have a suggestion engine attached.
1010        if (mSuggest == null || !settingsValues.isSuggestionsRequested()) {
1011            if (mWordComposer.isComposingWord()) {
1012                Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
1013                        + "requested!");
1014            }
1015            return;
1016        }
1017
1018        if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) {
1019            mLatinIME.setPunctuationSuggestions();
1020            return;
1021        }
1022
1023        final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
1024        mLatinIME.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
1025                SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
1026                    @Override
1027                    public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1028                        holder.set(suggestedWords);
1029                    }
1030                }
1031        );
1032
1033        // This line may cause the current thread to wait.
1034        final SuggestedWords suggestedWords = holder.get(null,
1035                Constants.GET_SUGGESTED_WORDS_TIMEOUT);
1036        if (suggestedWords != null) {
1037            mLatinIME.showSuggestionStrip(suggestedWords);
1038        }
1039    }
1040
1041    /**
1042     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
1043     * word, otherwise do nothing.
1044     * @param settingsValues the current values of the settings.
1045     */
1046    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
1047            final SettingsValues settingsValues,
1048            // TODO: remove these two arguments
1049            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
1050        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(settingsValues);
1051        if (null != word) {
1052            final String wordString = word.toString();
1053            mWordComposer.setComposingWord(word,
1054                    // Previous word is the 2nd word before cursor because we are restarting on the
1055                    // 1st word before cursor.
1056                    getNthPreviousWordForSuggestion(settingsValues, 2 /* nthPreviousWord */),
1057                    keyboardSwitcher.getKeyboard());
1058            final int length = word.length();
1059            mConnection.deleteSurroundingText(length, 0);
1060            mConnection.setComposingText(word, 1);
1061            handler.postUpdateSuggestionStrip();
1062            // TODO: Handle the case where the user manually moves the cursor and then backs up over
1063            // a separator. In that case, the current log unit should not be uncommitted.
1064            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1065                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
1066                        true /* dumpCurrentLogUnit */);
1067            }
1068        }
1069    }
1070
1071    /**
1072     * Check if the cursor is touching a word. If so, restart suggestions on this word, else
1073     * do nothing.
1074     *
1075     * @param settingsValues the current values of the settings.
1076     */
1077    // TODO: make this private.
1078    public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues,
1079            // TODO: Remove these argument.
1080            final KeyboardSwitcher keyboardSwitcher, final LatinIME.InputUpdater inputUpdater) {
1081        // HACK: We may want to special-case some apps that exhibit bad behavior in case of
1082        // recorrection. This is a temporary, stopgap measure that will be removed later.
1083        // TODO: remove this.
1084        if (settingsValues.isBrokenByRecorrection()) return;
1085        // A simple way to test for support from the TextView.
1086        if (!mLatinIME.isSuggestionsStripVisible()) return;
1087        // Recorrection is not supported in languages without spaces because we don't know
1088        // how to segment them yet.
1089        if (!settingsValues.mCurrentLanguageHasSpaces) return;
1090        // If the cursor is not touching a word, or if there is a selection, return right away.
1091        if (mLastSelectionStart != mLastSelectionEnd) return;
1092        // If we don't know the cursor location, return.
1093        if (mLastSelectionStart < 0) return;
1094        if (!mConnection.isCursorTouchingWord(settingsValues)) return;
1095        final TextRange range = mConnection.getWordRangeAtCursor(
1096                settingsValues.mWordSeparators, 0 /* additionalPrecedingWordsCount */);
1097        if (null == range) return; // Happens if we don't have an input connection at all
1098        if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
1099        // If for some strange reason (editor bug or so) we measure the text before the cursor as
1100        // longer than what the entire text is supposed to be, the safe thing to do is bail out.
1101        final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
1102        if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
1103        final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
1104        final String typedWord = range.mWord.toString();
1105        if (!isResumableWord(settingsValues, typedWord)) return;
1106        int i = 0;
1107        for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
1108            for (final String s : span.getSuggestions()) {
1109                ++i;
1110                if (!TextUtils.equals(s, typedWord)) {
1111                    suggestions.add(new SuggestedWordInfo(s,
1112                            SuggestionStripView.MAX_SUGGESTIONS - i,
1113                            SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
1114                            SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
1115                            SuggestedWordInfo.NOT_A_CONFIDENCE
1116                                    /* autoCommitFirstWordConfidence */));
1117                }
1118            }
1119        }
1120        mWordComposer.setComposingWord(typedWord,
1121                getNthPreviousWordForSuggestion(settingsValues,
1122                        // We want the previous word for suggestion. If we have chars in the word
1123                        // before the cursor, then we want the word before that, hence 2; otherwise,
1124                        // we want the word immediately before the cursor, hence 1.
1125                        0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
1126                keyboardSwitcher.getKeyboard());
1127        mWordComposer.setCursorPositionWithinWord(
1128                typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
1129        mConnection.setComposingRegion(mLastSelectionStart - numberOfCharsInWordBeforeCursor,
1130                mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
1131        if (suggestions.isEmpty()) {
1132            // We come here if there weren't any suggestion spans on this word. We will try to
1133            // compute suggestions for it instead.
1134            inputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
1135                    SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
1136                        @Override
1137                        public void onGetSuggestedWords(
1138                                final SuggestedWords suggestedWordsIncludingTypedWord) {
1139                            final SuggestedWords suggestedWords;
1140                            if (suggestedWordsIncludingTypedWord.size() > 1) {
1141                                // We were able to compute new suggestions for this word.
1142                                // Remove the typed word, since we don't want to display it in this
1143                                // case. The #getSuggestedWordsExcludingTypedWord() method sets
1144                                // willAutoCorrect to false.
1145                                suggestedWords = suggestedWordsIncludingTypedWord
1146                                        .getSuggestedWordsExcludingTypedWord();
1147                            } else {
1148                                // No saved suggestions, and we were unable to compute any good one
1149                                // either. Rather than displaying an empty suggestion strip, we'll
1150                                // display the original word alone in the middle.
1151                                // Since there is only one word, willAutoCorrect is false.
1152                                suggestedWords = suggestedWordsIncludingTypedWord;
1153                            }
1154                            // We need to pass typedWord because mWordComposer.mTypedWord may
1155                            // differ from typedWord.
1156                            mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
1157                                    suggestedWords, typedWord);
1158                        }});
1159        } else {
1160            // We found suggestion spans in the word. We'll create the SuggestedWords out of
1161            // them, and make willAutoCorrect false.
1162            final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
1163                    true /* typedWordValid */, false /* willAutoCorrect */,
1164                    false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
1165                    false /* isPrediction */);
1166            // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
1167            mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords,
1168                    typedWord);
1169        }
1170    }
1171
1172    /**
1173     * Reverts a previous commit with auto-correction.
1174     *
1175     * This is triggered upon pressing backspace just after a commit with auto-correction.
1176     *
1177     * @param settingsValues the current settings values.
1178     */
1179    private void revertCommit(final SettingsValues settingsValues,
1180            // TODO: remove these arguments
1181            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
1182        final String previousWord = mLastComposedWord.mPrevWord;
1183        final String originallyTypedWord = mLastComposedWord.mTypedWord;
1184        final String committedWord = mLastComposedWord.mCommittedWord;
1185        final int cancelLength = committedWord.length();
1186        // We want java chars, not codepoints for the following.
1187        final int separatorLength = mLastComposedWord.mSeparatorString.length();
1188        // TODO: should we check our saved separator against the actual contents of the text view?
1189        final int deleteLength = cancelLength + separatorLength;
1190        if (LatinImeLogger.sDBG) {
1191            if (mWordComposer.isComposingWord()) {
1192                throw new RuntimeException("revertCommit, but we are composing a word");
1193            }
1194            final CharSequence wordBeforeCursor =
1195                    mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength);
1196            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
1197                throw new RuntimeException("revertCommit check failed: we thought we were "
1198                        + "reverting \"" + committedWord
1199                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
1200            }
1201        }
1202        mConnection.deleteSurroundingText(deleteLength, 0);
1203        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
1204            if (mSuggest != null) {
1205                mSuggest.cancelAddingUserHistory(previousWord, committedWord);
1206            }
1207        }
1208        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
1209        if (settingsValues.mCurrentLanguageHasSpaces) {
1210            // For languages with spaces, we revert to the typed string, but the cursor is still
1211            // after the separator so we don't resume suggestions. If the user wants to correct
1212            // the word, they have to press backspace again.
1213            mConnection.commitText(stringToCommit, 1);
1214        } else {
1215            // For languages without spaces, we revert the typed string but the cursor is flush
1216            // with the typed word, so we need to resume suggestions right away.
1217            mWordComposer.setComposingWord(stringToCommit, previousWord,
1218                    keyboardSwitcher.getKeyboard());
1219            mConnection.setComposingText(stringToCommit, 1);
1220        }
1221        if (settingsValues.mIsInternal) {
1222            LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
1223                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
1224        }
1225        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1226            ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
1227                    mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
1228        }
1229        // Don't restart suggestion yet. We'll restart if the user deletes the
1230        // separator.
1231        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1232        // We have a separator between the word and the cursor: we should show predictions.
1233        handler.postUpdateSuggestionStrip();
1234    }
1235
1236    /**
1237     * Factor in auto-caps and manual caps and compute the current caps mode.
1238     * @param settingsValues the current settings values.
1239     * @param keyboardSwitcher the keyboard switcher. Caps mode depends on its mode.
1240     * @return the actual caps mode the keyboard is in right now.
1241     */
1242    public int getActualCapsMode(final SettingsValues settingsValues,
1243            final KeyboardSwitcher keyboardSwitcher) {
1244        final int keyboardShiftMode = keyboardSwitcher.getKeyboardShiftMode();
1245        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
1246        final int auto = getCurrentAutoCapsState(settingsValues);
1247        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
1248            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
1249        }
1250        if (0 != auto) {
1251            return WordComposer.CAPS_MODE_AUTO_SHIFTED;
1252        }
1253        return WordComposer.CAPS_MODE_OFF;
1254    }
1255
1256    /**
1257     * Gets the current auto-caps state, factoring in the space state.
1258     *
1259     * This method tries its best to do this in the most efficient possible manner. It avoids
1260     * getting text from the editor if possible at all.
1261     * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
1262     * needs to know auto caps state to display the right layout.
1263     *
1264     * @param optionalSettingsValues settings values, or null if we should just get the current ones
1265     *   from the singleton.
1266     * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
1267     */
1268    public int getCurrentAutoCapsState(final SettingsValues optionalSettingsValues) {
1269        // If we are in a batch edit, we need to use the same settings values as the outside
1270        // code, that will pass it to us. Otherwise, we can just take the current values.
1271        final SettingsValues settingsValues = null != optionalSettingsValues
1272                ? optionalSettingsValues : Settings.getInstance().getCurrent();
1273        if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
1274
1275        final EditorInfo ei = getCurrentInputEditorInfo();
1276        if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
1277        final int inputType = ei.inputType;
1278        // Warning: this depends on mSpaceState, which may not be the most current value. If
1279        // mSpaceState gets updated later, whoever called this may need to be told about it.
1280        return mConnection.getCursorCapsMode(inputType, settingsValues,
1281                SpaceState.PHANTOM == mSpaceState);
1282    }
1283
1284    public int getCurrentRecapitalizeState() {
1285        if (!mRecapitalizeStatus.isActive()
1286                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
1287            // Not recapitalizing at the moment
1288            return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
1289        }
1290        return mRecapitalizeStatus.getCurrentMode();
1291    }
1292
1293    /**
1294     * @return the editor info for the current editor
1295     */
1296    private EditorInfo getCurrentInputEditorInfo() {
1297        return mLatinIME.getCurrentInputEditorInfo();
1298    }
1299
1300    /**
1301     * Get the nth previous word before the cursor as context for the suggestion process.
1302     * @param currentSettings the current settings values.
1303     * @param nthPreviousWord reverse index of the word to get (1-indexed)
1304     * @return the nth previous word before the cursor.
1305     */
1306    // TODO: Make this private
1307    public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
1308            final int nthPreviousWord) {
1309        if (currentSettings.mCurrentLanguageHasSpaces) {
1310            // If we are typing in a language with spaces we can just look up the previous
1311            // word from textview.
1312            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
1313        } else {
1314            return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
1315                    : mLastComposedWord.mCommittedWord;
1316        }
1317    }
1318
1319    /**
1320     * Tests the passed word for resumability.
1321     *
1322     * We can resume suggestions on words whose first code point is a word code point (with some
1323     * nuances: check the code for details).
1324     *
1325     * @param settings the current values of the settings.
1326     * @param word the word to evaluate.
1327     * @return whether it's fine to resume suggestions on this word.
1328     */
1329    private static boolean isResumableWord(final SettingsValues settings, final String word) {
1330        final int firstCodePoint = word.codePointAt(0);
1331        return settings.isWordCodePoint(firstCodePoint)
1332                && Constants.CODE_SINGLE_QUOTE != firstCodePoint
1333                && Constants.CODE_DASH != firstCodePoint;
1334    }
1335
1336    /**
1337     * @param actionId the action to perform
1338     */
1339    private void performEditorAction(final int actionId) {
1340        mConnection.performEditorAction(actionId);
1341    }
1342
1343    /**
1344     * Perform the processing specific to inputting TLDs.
1345     *
1346     * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
1347     * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
1348     * of character in onCodeInput, but since this gets inputted as a whole string we need to
1349     * do it here specifically. Then, if the last character before the cursor is a period, then
1350     * we cut the dot at the start of ".com". This is because humans tend to type "www.google."
1351     * and then press the ".com" key and instinctively don't expect to get "www.google..com".
1352     *
1353     * @param text the raw text supplied to onTextInput
1354     * @return the text to actually send to the editor
1355     */
1356    private String performSpecificTldProcessingOnTextInput(final String text) {
1357        if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
1358                || !Character.isLetter(text.charAt(1))) {
1359            // Not a tld: do nothing.
1360            return text;
1361        }
1362        // We have a TLD (or something that looks like this): make sure we don't add
1363        // a space even if currently in phantom mode.
1364        mSpaceState = SpaceState.NONE;
1365        final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1366        // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
1367        if (Constants.CODE_PERIOD == codePointBeforeCursor) {
1368            return text.substring(1);
1369        } else {
1370            return text;
1371        }
1372    }
1373
1374    /**
1375     * Handle a press on the settings key.
1376     */
1377    private void onSettingsKeyPressed() {
1378        mLatinIME.displaySettingsDialog();
1379    }
1380
1381    /**
1382     * Resets the whole input state to the starting state.
1383     *
1384     * This will clear the composing word, reset the last composed word, clear the suggestion
1385     * strip and tell the input connection about it so that it can refresh its caches.
1386     *
1387     * @param settingsValues the current values of the settings.
1388     * @param newSelStart the new selection start, in java characters.
1389     * @param newSelEnd the new selection end, in java characters.
1390     */
1391    // TODO: how is this different from startInput ?!
1392    // TODO: remove all references to this in LatinIME and make this private
1393    public void resetEntireInputState(final SettingsValues settingsValues,
1394            final int newSelStart, final int newSelEnd) {
1395        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
1396        resetComposingState(true /* alsoResetLastComposedWord */);
1397        if (settingsValues.mBigramPredictionEnabled) {
1398            mLatinIME.clearSuggestionStrip();
1399        } else {
1400            mLatinIME.setSuggestedWords(settingsValues.mSuggestPuncList, false);
1401        }
1402        mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
1403                shouldFinishComposition);
1404    }
1405
1406    /**
1407     * Resets only the composing state.
1408     *
1409     * Compare #resetEntireInputState, which also clears the suggestion strip and resets the
1410     * input connection caches. This only deals with the composing state.
1411     *
1412     * @param alsoResetLastComposedWord whether to also reset the last composed word.
1413     */
1414    // TODO: remove all references to this in LatinIME and make this private.
1415    public void resetComposingState(final boolean alsoResetLastComposedWord) {
1416        mWordComposer.reset();
1417        if (alsoResetLastComposedWord) {
1418            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1419        }
1420    }
1421
1422    /**
1423     * Gets a chunk of text with or the auto-correction indicator underline span as appropriate.
1424     *
1425     * This method looks at the old state of the auto-correction indicator to put or not put
1426     * the underline span as appropriate. It is important to note that this does not correspond
1427     * exactly to whether this word will be auto-corrected to or not: what's important here is
1428     * to keep the same indication as before.
1429     * When we add a new code point to a composing word, we don't know yet if we are going to
1430     * auto-correct it until the suggestions are computed. But in the mean time, we still need
1431     * to display the character and to extend the previous underline. To avoid any flickering,
1432     * the underline should keep the same color it used to have, even if that's not ultimately
1433     * the correct color for this new word. When the suggestions are finished evaluating, we
1434     * will call this method again to fix the color of the underline.
1435     *
1436     * @param text the text on which to maybe apply the span.
1437     * @return the same text, with the auto-correction underline span if that's appropriate.
1438     */
1439    // TODO: remove all references to this in LatinIME and make this private. Also, shouldn't
1440    // this go in some *Utils class instead?
1441    public CharSequence getTextWithUnderline(final String text) {
1442        return mIsAutoCorrectionIndicatorOn
1443                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
1444                : text;
1445    }
1446
1447    /**
1448     * Sends a DOWN key event followed by an UP key event to the editor.
1449     *
1450     * If possible at all, avoid using this method. It causes all sorts of race conditions with
1451     * the text view because it goes through a different, asynchronous binder. Also, batch edits
1452     * are ignored for key events. Use the normal software input methods instead.
1453     *
1454     * @param keyCode the key code to send inside the key event.
1455     */
1456    private void sendDownUpKeyEvent(final int keyCode) {
1457        final long eventTime = SystemClock.uptimeMillis();
1458        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
1459                KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1460                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1461        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
1462                KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1463                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1464    }
1465
1466    /**
1467     * Sends a code point to the editor, using the most appropriate method.
1468     *
1469     * Normally we send code points with commitText, but there are some cases (where backward
1470     * compatibility is a concern for example) where we want to use deprecated methods.
1471     *
1472     * @param settingsValues the current values of the settings.
1473     * @param codePoint the code point to send.
1474     */
1475    private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) {
1476        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1477            ResearchLogger.latinIME_sendKeyCodePoint(codePoint);
1478        }
1479        // TODO: Remove this special handling of digit letters.
1480        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1481        if (codePoint >= '0' && codePoint <= '9') {
1482            sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
1483            return;
1484        }
1485
1486        // TODO: we should do this also when the editor has TYPE_NULL
1487        if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) {
1488            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1489            // a hardware keyboard event on pressing enter or delete. This is bad for many
1490            // reasons (there are race conditions with commits) but some applications are
1491            // relying on this behavior so we continue to support it for older apps.
1492            sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
1493        } else {
1494            mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
1495        }
1496    }
1497
1498    /**
1499     * Promote a phantom space to an actual space.
1500     *
1501     * This essentially inserts a space, and that's it. It just checks the options and the text
1502     * before the cursor are appropriate before doing it.
1503     *
1504     * @param settingsValues the current values of the settings.
1505     */
1506    // TODO: Make this private.
1507    public void promotePhantomSpace(final SettingsValues settingsValues) {
1508        if (settingsValues.shouldInsertSpacesAutomatically()
1509                && settingsValues.mCurrentLanguageHasSpaces
1510                && !mConnection.textBeforeCursorLooksLikeURL()) {
1511            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1512                ResearchLogger.latinIME_promotePhantomSpace();
1513            }
1514            sendKeyCodePoint(settingsValues, Constants.CODE_SPACE);
1515        }
1516    }
1517
1518    /**
1519     * Commit the typed string to the editor.
1520     *
1521     * This is typically called when we should commit the currently composing word without applying
1522     * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard
1523     * is configured to not do auto-correction at all (because of the settings or the properties of
1524     * the editor). In this case, `separatorString' is set to the separator that was pressed.
1525     * We also come here in a variety of cases with external user action. For example, when the
1526     * cursor is moved while there is a composition, or when the keyboard is closed, or when the
1527     * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected.
1528     * In this case, `separatorString' is set to NOT_A_SEPARATOR.
1529     *
1530     * @param settingsValues the current values of the settings.
1531     * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
1532     */
1533    // TODO: Make this private
1534    public void commitTyped(final SettingsValues settingsValues, final String separatorString) {
1535        if (!mWordComposer.isComposingWord()) return;
1536        final String typedWord = mWordComposer.getTypedWord();
1537        if (typedWord.length() > 0) {
1538            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1539                ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
1540            }
1541            commitChosenWord(settingsValues, typedWord,
1542                    LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString);
1543        }
1544    }
1545
1546    /**
1547     * Commit the current auto-correction.
1548     *
1549     * This will commit the best guess of the keyboard regarding what the user meant by typing
1550     * the currently composing word. The IME computes suggestions and assigns a confidence score
1551     * to each of them; when it's confident enough in one suggestion, it replaces the typed string
1552     * by this suggestion at commit time. When it's not confident enough, or when it has no
1553     * suggestions, or when the settings or environment does not allow for auto-correction, then
1554     * this method just commits the typed string.
1555     * Note that if suggestions are currently being computed in the background, this method will
1556     * block until the computation returns. This is necessary for consistency (it would be very
1557     * strange if pressing space would commit a different word depending on how fast you press).
1558     *
1559     * @param settingsValues the current value of the settings.
1560     * @param separator the separator that's causing the commit to happen.
1561     */
1562    // TODO: Make this private
1563    public void commitCurrentAutoCorrection(final SettingsValues settingsValues,
1564            final String separator,
1565            // TODO: Remove this argument.
1566            final LatinIME.UIHandler handler) {
1567        // Complete any pending suggestions query first
1568        if (handler.hasPendingUpdateSuggestions()) {
1569            performUpdateSuggestionStripSync(settingsValues, handler);
1570        }
1571        final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
1572        final String typedWord = mWordComposer.getTypedWord();
1573        final String autoCorrection = (typedAutoCorrection != null)
1574                ? typedAutoCorrection : typedWord;
1575        if (autoCorrection != null) {
1576            if (TextUtils.isEmpty(typedWord)) {
1577                throw new RuntimeException("We have an auto-correction but the typed word "
1578                        + "is empty? Impossible! I must commit suicide.");
1579            }
1580            if (settingsValues.mIsInternal) {
1581                LatinImeLoggerUtils.onAutoCorrection(
1582                        typedWord, autoCorrection, separator, mWordComposer);
1583            }
1584            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1585                final SuggestedWords suggestedWords = mSuggestedWords;
1586                ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
1587                        separator, mWordComposer.isBatchMode(), suggestedWords);
1588            }
1589            commitChosenWord(settingsValues, autoCorrection,
1590                    LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator);
1591            if (!typedWord.equals(autoCorrection)) {
1592                // This will make the correction flash for a short while as a visual clue
1593                // to the user that auto-correction happened. It has no other effect; in particular
1594                // note that this won't affect the text inside the text field AT ALL: it only makes
1595                // the segment of text starting at the supplied index and running for the length
1596                // of the auto-correction flash. At this moment, the "typedWord" argument is
1597                // ignored by TextView.
1598                mConnection.commitCorrection(
1599                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
1600                        typedWord, autoCorrection));
1601            }
1602        }
1603    }
1604
1605    /**
1606     * Commits the chosen word to the text field and saves it for later retrieval.
1607     *
1608     * @param settingsValues the current values of the settings.
1609     * @param chosenWord the word we want to commit.
1610     * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
1611     * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
1612     */
1613    // TODO: Make this private
1614    public void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
1615            final int commitType, final String separatorString) {
1616        final SuggestedWords suggestedWords = mSuggestedWords;
1617        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
1618                suggestedWords), 1);
1619        // Add the word to the user history dictionary
1620        final String prevWord = performAdditionToUserHistoryDictionary(settingsValues, chosenWord);
1621        // TODO: figure out here if this is an auto-correct or if the best word is actually
1622        // what user typed. Note: currently this is done much later in
1623        // LastComposedWord#didCommitTypedWord by string equality of the remembered
1624        // strings.
1625        mLastComposedWord = mWordComposer.commitWord(commitType,
1626                chosenWord, separatorString, prevWord);
1627        final boolean shouldDiscardPreviousWordForSuggestion;
1628        if (0 == StringUtils.codePointCount(separatorString)) {
1629            // Separator is 0-length. Discard the word only if the current language has spaces.
1630            shouldDiscardPreviousWordForSuggestion = settingsValues.mCurrentLanguageHasSpaces;
1631        } else {
1632            // Otherwise, we discard if the separator contains any non-whitespace.
1633            shouldDiscardPreviousWordForSuggestion =
1634                    !StringUtils.containsOnlyWhitespace(separatorString);
1635        }
1636        if (shouldDiscardPreviousWordForSuggestion) {
1637            mWordComposer.discardPreviousWordForSuggestion();
1638        }
1639    }
1640
1641    /**
1642     * Try to get the text from the editor to expose lies the framework may have been
1643     * telling us. Concretely, when the device rotates, the frameworks tells us about where the
1644     * cursor used to be initially in the editor at the time it first received the focus; this
1645     * may be completely different from the place it is upon rotation. Since we don't have any
1646     * means to get the real value, try at least to ask the text view for some characters and
1647     * detect the most damaging cases: when the cursor position is declared to be much smaller
1648     * than it really is.
1649     */
1650    // TODO: make this private
1651    public void tryFixLyingCursorPosition() {
1652        final CharSequence textBeforeCursor = mConnection.getTextBeforeCursor(
1653                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
1654        if (null == textBeforeCursor) {
1655            mLastSelectionStart = mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
1656        } else {
1657            final int textLength = textBeforeCursor.length();
1658            if (textLength > mLastSelectionStart
1659                    || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
1660                            && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
1661                // It should not be possible to have only one of those variables be
1662                // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
1663                // (simple cursor, no selection) or there is no cursor/we don't know its pos
1664                final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
1665                mLastSelectionStart = textLength;
1666                // We can't figure out the value of mLastSelectionEnd :(
1667                // But at least if it's smaller than mLastSelectionStart something is wrong,
1668                // and if they used to be equal we also don't want to make it look like there is a
1669                // selection.
1670                if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
1671                    mLastSelectionEnd = mLastSelectionStart;
1672                }
1673            }
1674        }
1675    }
1676
1677    /**
1678     * Retry resetting caches in the rich input connection.
1679     *
1680     * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
1681     * This method handles the retry, and re-schedules a new retry if we still can't access.
1682     * We only retry up to 5 times before giving up.
1683     *
1684     * @param settingsValues the current values of the settings.
1685     * @param tryResumeSuggestions Whether we should resume suggestions or not.
1686     * @param remainingTries How many times we may try again before giving up.
1687     */
1688    // TODO: make this private
1689    public void retryResetCaches(final SettingsValues settingsValues,
1690            final boolean tryResumeSuggestions, final int remainingTries,
1691            // TODO: remove these arguments
1692            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
1693        if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
1694                mLastSelectionStart, mLastSelectionEnd, false)) {
1695            if (0 < remainingTries) {
1696                handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
1697                return;
1698            }
1699            // If remainingTries is 0, we should stop waiting for new tries, but it's still
1700            // better to load the keyboard (less things will be broken).
1701        }
1702        tryFixLyingCursorPosition();
1703        keyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), settingsValues);
1704        if (tryResumeSuggestions) {
1705            handler.postResumeSuggestions();
1706        }
1707    }
1708}
1709