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