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