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