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