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