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