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 
17 package com.android.inputmethod.latin.inputlogic;
18 
19 import android.graphics.Color;
20 import android.inputmethodservice.InputMethodService;
21 import android.os.SystemClock;
22 import android.text.SpannableString;
23 import android.text.Spanned;
24 import android.text.TextUtils;
25 import android.text.style.BackgroundColorSpan;
26 import android.text.style.SuggestionSpan;
27 import android.util.Log;
28 import android.view.KeyCharacterMap;
29 import android.view.KeyEvent;
30 import android.view.inputmethod.CorrectionInfo;
31 import android.view.inputmethod.CursorAnchorInfo;
32 import android.view.inputmethod.EditorInfo;
33 
34 import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
35 import com.android.inputmethod.compat.SuggestionSpanUtils;
36 import com.android.inputmethod.event.Event;
37 import com.android.inputmethod.event.InputTransaction;
38 import com.android.inputmethod.keyboard.KeyboardSwitcher;
39 import com.android.inputmethod.keyboard.ProximityInfo;
40 import com.android.inputmethod.keyboard.TextDecorator;
41 import com.android.inputmethod.keyboard.TextDecoratorUiOperator;
42 import com.android.inputmethod.latin.Constants;
43 import com.android.inputmethod.latin.Dictionary;
44 import com.android.inputmethod.latin.DictionaryFacilitator;
45 import com.android.inputmethod.latin.InputPointers;
46 import com.android.inputmethod.latin.LastComposedWord;
47 import com.android.inputmethod.latin.LatinIME;
48 import com.android.inputmethod.latin.PrevWordsInfo;
49 import com.android.inputmethod.latin.RichInputConnection;
50 import com.android.inputmethod.latin.Suggest;
51 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
52 import com.android.inputmethod.latin.SuggestedWords;
53 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
54 import com.android.inputmethod.latin.WordComposer;
55 import com.android.inputmethod.latin.define.DebugFlags;
56 import com.android.inputmethod.latin.define.ProductionFlags;
57 import com.android.inputmethod.latin.settings.SettingsValues;
58 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
59 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
60 import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
61 import com.android.inputmethod.latin.utils.AsyncResultHolder;
62 import com.android.inputmethod.latin.utils.InputTypeUtils;
63 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
64 import com.android.inputmethod.latin.utils.StringUtils;
65 import com.android.inputmethod.latin.utils.TextRange;
66 
67 import java.util.ArrayList;
68 import java.util.TreeSet;
69 import java.util.concurrent.TimeUnit;
70 
71 /**
72  * This class manages the input logic.
73  */
74 public final class InputLogic {
75     private static final String TAG = InputLogic.class.getSimpleName();
76 
77     // TODO : Remove this member when we can.
78     private final LatinIME mLatinIME;
79     private final SuggestionStripViewAccessor mSuggestionStripViewAccessor;
80 
81     // Never null.
82     private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
83 
84     // TODO : make all these fields private as soon as possible.
85     // Current space state of the input method. This can be any of the above constants.
86     private int mSpaceState;
87     // Never null
88     public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
89     public final Suggest mSuggest;
90     private final DictionaryFacilitator mDictionaryFacilitator;
91 
92     private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() {
93         @Override
94         public void onClickComposingTextToAddToDictionary(final String word) {
95             mLatinIME.addWordToUserDictionary(word);
96             mLatinIME.dismissAddToDictionaryHint();
97         }
98     });
99 
100     public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
101     // This has package visibility so it can be accessed from InputLogicHandler.
102     /* package */ final WordComposer mWordComposer;
103     public final RichInputConnection mConnection;
104     private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
105 
106     private int mDeleteCount;
107     private long mLastKeyTime;
108     public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();
109 
110     // Keeps track of most recently inserted text (multi-character key) for reverting
111     private String mEnteredText;
112 
113     // TODO: This boolean is persistent state and causes large side effects at unexpected times.
114     // Find a way to remove it for readability.
115     private boolean mIsAutoCorrectionIndicatorOn;
116     private long mDoubleSpacePeriodCountdownStart;
117 
118     /**
119      * Create a new instance of the input logic.
120      * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
121      * @param suggestionStripViewAccessor an object to access the suggestion strip view.
122      * @param dictionaryFacilitator facilitator for getting suggestions and updating user history
123      * dictionary.
124      */
InputLogic(final LatinIME latinIME, final SuggestionStripViewAccessor suggestionStripViewAccessor, final DictionaryFacilitator dictionaryFacilitator)125     public InputLogic(final LatinIME latinIME,
126             final SuggestionStripViewAccessor suggestionStripViewAccessor,
127             final DictionaryFacilitator dictionaryFacilitator) {
128         mLatinIME = latinIME;
129         mSuggestionStripViewAccessor = suggestionStripViewAccessor;
130         mWordComposer = new WordComposer();
131         mConnection = new RichInputConnection(latinIME);
132         mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
133         mSuggest = new Suggest(dictionaryFacilitator);
134         mDictionaryFacilitator = dictionaryFacilitator;
135     }
136 
137     /**
138      * Initializes the input logic for input in an editor.
139      *
140      * Call this when input starts or restarts in some editor (typically, in onStartInputView).
141      *
142      * @param combiningSpec the combining spec string for this subtype
143      * @param settingsValues the current settings values
144      */
startInput(final String combiningSpec, final SettingsValues settingsValues)145     public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
146         mEnteredText = null;
147         mWordComposer.restartCombining(combiningSpec);
148         resetComposingState(true /* alsoResetLastComposedWord */);
149         mDeleteCount = 0;
150         mSpaceState = SpaceState.NONE;
151         mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
152         mCurrentlyPressedHardwareKeys.clear();
153         mSuggestedWords = SuggestedWords.EMPTY;
154         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
155         // so we try using some heuristics to find out about these and fix them.
156         mConnection.tryFixLyingCursorPosition();
157         cancelDoubleSpacePeriodCountdown();
158         if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) {
159             mInputLogicHandler = new InputLogicHandler(mLatinIME, this);
160         } else {
161             mInputLogicHandler.reset();
162         }
163 
164         if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) {
165             // AcceptTypedWord feature relies on CursorAnchorInfo.
166             if (settingsValues.mShouldShowUiToAcceptTypedWord) {
167                 mConnection.requestCursorUpdates(true /* enableMonitor */,
168                         true /* requestImmediateCallback */);
169             }
170             mTextDecorator.reset();
171         }
172     }
173 
174     /**
175      * Call this when the subtype changes.
176      * @param combiningSpec the spec string for the combining rules
177      * @param settingsValues the current settings values
178      */
onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues)179     public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) {
180         finishInput();
181         startInput(combiningSpec, settingsValues);
182     }
183 
184     /**
185      * Call this when the orientation changes.
186      * @param settingsValues the current values of the settings.
187      */
onOrientationChange(final SettingsValues settingsValues)188     public void onOrientationChange(final SettingsValues settingsValues) {
189         // If !isComposingWord, #commitTyped() is a no-op, but still, it's better to avoid
190         // the useless IPC of {begin,end}BatchEdit.
191         if (mWordComposer.isComposingWord()) {
192             mConnection.beginBatchEdit();
193             // If we had a composition in progress, we need to commit the word so that the
194             // suggestionsSpan will be added. This will allow resuming on the same suggestions
195             // after rotation is finished.
196             commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
197             mConnection.endBatchEdit();
198         }
199     }
200 
201     /**
202      * Clean up the input logic after input is finished.
203      */
finishInput()204     public void finishInput() {
205         if (mWordComposer.isComposingWord()) {
206             mConnection.finishComposingText();
207         }
208         resetComposingState(true /* alsoResetLastComposedWord */);
209         mInputLogicHandler.reset();
210     }
211 
212     // Normally this class just gets out of scope after the process ends, but in unit tests, we
213     // create several instances of LatinIME in the same process, which results in several
214     // instances of InputLogic. This cleans up the associated handler so that tests don't leak
215     // handlers.
recycle()216     public void recycle() {
217         final InputLogicHandler inputLogicHandler = mInputLogicHandler;
218         mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
219         inputLogicHandler.destroy();
220         mDictionaryFacilitator.closeDictionaries();
221     }
222 
223     /**
224      * React to a string input.
225      *
226      * This is triggered by keys that input many characters at once, like the ".com" key or
227      * some additional keys for example.
228      *
229      * @param settingsValues the current values of the settings.
230      * @param event the input event containing the data.
231      * @return the complete transaction object
232      */
onTextInput(final SettingsValues settingsValues, final Event event, final int keyboardShiftMode, final LatinIME.UIHandler handler)233     public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event,
234             final int keyboardShiftMode,
235             // TODO: remove this argument
236             final LatinIME.UIHandler handler) {
237         final String rawText = event.getTextToCommit().toString();
238         final InputTransaction inputTransaction = new InputTransaction(settingsValues, event,
239                 SystemClock.uptimeMillis(), mSpaceState,
240                 getActualCapsMode(settingsValues, keyboardShiftMode));
241         mConnection.beginBatchEdit();
242         if (mWordComposer.isComposingWord()) {
243             commitCurrentAutoCorrection(settingsValues, rawText, handler);
244         } else {
245             resetComposingState(true /* alsoResetLastComposedWord */);
246         }
247         handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING);
248         final String text = performSpecificTldProcessingOnTextInput(rawText);
249         if (SpaceState.PHANTOM == mSpaceState) {
250             promotePhantomSpace(settingsValues);
251         }
252         mConnection.commitText(text, 1);
253         mConnection.endBatchEdit();
254         // Space state must be updated before calling updateShiftState
255         mSpaceState = SpaceState.NONE;
256         mEnteredText = text;
257         inputTransaction.setDidAffectContents();
258         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
259         return inputTransaction;
260     }
261 
262     /**
263      * Determines whether "Touch again to save" should be shown or not.
264      * @param suggestionInfo the suggested word chosen by the user.
265      * @return {@code true} if we should show the "Touch again to save" hint.
266      */
shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo)267     private boolean shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo) {
268         // We should show the "Touch again to save" hint if the user pressed the first entry
269         // AND it's in none of our current dictionaries (main, user or otherwise).
270         return (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED)
271                 || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION))
272                 && !mDictionaryFacilitator.isValidWord(suggestionInfo.mWord, true /* ignoreCase */)
273                 && mDictionaryFacilitator.isUserDictionaryEnabled();
274     }
275 
276     /**
277      * A suggestion was picked from the suggestion strip.
278      * @param settingsValues the current values of the settings.
279      * @param suggestionInfo the suggestion info.
280      * @param keyboardShiftState the shift state of the keyboard, as returned by
281      *     {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
282      * @return the complete transaction object
283      */
284     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
285     // interface
onPickSuggestionManually(final SettingsValues settingsValues, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)286     public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues,
287             final SuggestedWordInfo suggestionInfo, final int keyboardShiftState,
288             // TODO: remove these arguments
289             final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
290         final SuggestedWords suggestedWords = mSuggestedWords;
291         final String suggestion = suggestionInfo.mWord;
292         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
293         if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) {
294             // Word separators are suggested before the user inputs something.
295             // Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
296             final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo);
297             return onCodeInput(settingsValues, event, keyboardShiftState,
298                     currentKeyboardScriptId, handler);
299         }
300 
301         final Event event = Event.createSuggestionPickedEvent(suggestionInfo);
302         final InputTransaction inputTransaction = new InputTransaction(settingsValues,
303                 event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState);
304         // Manual pick affects the contents of the editor, so we take note of this. It's important
305         // for the sequence of language switching.
306         inputTransaction.setDidAffectContents();
307         mConnection.beginBatchEdit();
308         if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0
309                 // In the batch input mode, a manually picked suggested word should just replace
310                 // the current batch input text and there is no need for a phantom space.
311                 && !mWordComposer.isBatchMode()) {
312             final int firstChar = Character.codePointAt(suggestion, 0);
313             if (!settingsValues.isWordSeparator(firstChar)
314                     || settingsValues.isUsuallyPrecededBySpace(firstChar)) {
315                 promotePhantomSpace(settingsValues);
316             }
317         }
318 
319         // TODO: We should not need the following branch. We should be able to take the same
320         // code path as for other kinds, use commitChosenWord, and do everything normally. We will
321         // however need to reset the suggestion strip right away, because we know we can't take
322         // the risk of calling commitCompletion twice because we don't know how the app will react.
323         if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) {
324             mSuggestedWords = SuggestedWords.EMPTY;
325             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
326             inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
327             resetComposingState(true /* alsoResetLastComposedWord */);
328             mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo);
329             mConnection.endBatchEdit();
330             return inputTransaction;
331         }
332 
333         final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo);
334         commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
335                 LastComposedWord.NOT_A_SEPARATOR);
336         mConnection.endBatchEdit();
337         // Don't allow cancellation of manual pick
338         mLastComposedWord.deactivate();
339         // Space state must be updated before calling updateShiftState
340         mSpaceState = SpaceState.PHANTOM;
341         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
342 
343         if (shouldShowAddToDictionaryHint) {
344             mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion);
345         } else {
346             // If we're not showing the "Touch again to save", then update the suggestion strip.
347             // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE.
348             handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE);
349         }
350         return inputTransaction;
351     }
352 
353     /**
354      * Consider an update to the cursor position. Evaluate whether this update has happened as
355      * part of normal typing or whether it was an explicit cursor move by the user. In any case,
356      * do the necessary adjustments.
357      * @param oldSelStart old selection start
358      * @param oldSelEnd old selection end
359      * @param newSelStart new selection start
360      * @param newSelEnd new selection end
361      * @param settingsValues the current values of the settings.
362      * @return whether the cursor has moved as a result of user interaction.
363      */
onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final SettingsValues settingsValues)364     public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd,
365             final int newSelStart, final int newSelEnd, final SettingsValues settingsValues) {
366         if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) {
367             return false;
368         }
369         // TODO: the following is probably better done in resetEntireInputState().
370         // it should only happen when the cursor moved, and the very purpose of the
371         // test below is to narrow down whether this happened or not. Likewise with
372         // the call to updateShiftState.
373         // We set this to NONE because after a cursor move, we don't want the space
374         // state-related special processing to kick in.
375         mSpaceState = SpaceState.NONE;
376 
377         final boolean selectionChangedOrSafeToReset =
378                 oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed
379                 || !mWordComposer.isComposingWord(); // safe to reset
380         final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd);
381         final int moveAmount = newSelStart - oldSelStart;
382         // As an added small gift from the framework, it happens upon rotation when there
383         // is a selection that we get a wrong cursor position delivered to startInput() that
384         // does not get reflected in the oldSel{Start,End} parameters to the next call to
385         // onUpdateSelection. In this case, we may have set a composition, and when we're here
386         // we realize we shouldn't have. In theory, in this case, selectionChangedOrSafeToReset
387         // should be true, but that is if the framework had taken that wrong cursor position
388         // into account, which means we have to reset the entire composing state whenever there
389         // is or was a selection regardless of whether it changed or not.
390         if (hasOrHadSelection || !settingsValues.needsToLookupSuggestions()
391                 || (selectionChangedOrSafeToReset
392                         && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
393             // If we are composing a word and moving the cursor, we would want to set a
394             // suggestion span for recorrection to work correctly. Unfortunately, that
395             // would involve the keyboard committing some new text, which would move the
396             // cursor back to where it was. Latin IME could then fix the position of the cursor
397             // again, but the asynchronous nature of the calls results in this wreaking havoc
398             // with selection on double tap and the like.
399             // Another option would be to send suggestions each time we set the composing
400             // text, but that is probably too expensive to do, so we decided to leave things
401             // as is.
402             // Also, we're posting a resume suggestions message, and this will update the
403             // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here
404             // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear
405             // it here, which means we'll keep outdated suggestions for a split second but the
406             // visual result is better.
407             resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */);
408         } else {
409             // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
410             // composition to end. But in all cases where we don't reset the entire input
411             // state, we still want to tell the rich input connection about the new cursor
412             // position so that it can update its caches.
413             mConnection.resetCachesUponCursorMoveAndReturnSuccess(
414                     newSelStart, newSelEnd, false /* shouldFinishComposition */);
415         }
416 
417         // The cursor has been moved : we now accept to perform recapitalization
418         mRecapitalizeStatus.enable();
419         // We moved the cursor and need to invalidate the indicator right now.
420         mTextDecorator.reset();
421         // Remaining background color that was used for the add-to-dictionary indicator should be
422         // removed.
423         mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
424         // We moved the cursor. If we are touching a word, we need to resume suggestion.
425         mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */,
426                 true /* shouldDelay */);
427         // Stop the last recapitalization, if started.
428         mRecapitalizeStatus.stop();
429         return true;
430     }
431 
432     /**
433      * React to a code input. It may be a code point to insert, or a symbolic value that influences
434      * the keyboard behavior.
435      *
436      * Typically, this is called whenever a key is pressed on the software keyboard. This is not
437      * the entry point for gesture input; see the onBatchInput* family of functions for this.
438      *
439      * @param settingsValues the current settings values.
440      * @param event the event to handle.
441      * @param keyboardShiftMode the current shift mode of the keyboard, as returned by
442      *     {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
443      * @return the complete transaction object
444      */
onCodeInput(final SettingsValues settingsValues, final Event event, final int keyboardShiftMode, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)445     public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event,
446             final int keyboardShiftMode,
447             // TODO: remove these arguments
448             final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
449         final Event processedEvent = mWordComposer.processEvent(event);
450         final InputTransaction inputTransaction = new InputTransaction(settingsValues,
451                 processedEvent, SystemClock.uptimeMillis(), mSpaceState,
452                 getActualCapsMode(settingsValues, keyboardShiftMode));
453         if (processedEvent.mKeyCode != Constants.CODE_DELETE
454                 || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
455             mDeleteCount = 0;
456         }
457         mLastKeyTime = inputTransaction.mTimestamp;
458         mConnection.beginBatchEdit();
459         if (!mWordComposer.isComposingWord()) {
460             // TODO: is this useful? It doesn't look like it should be done here, but rather after
461             // a word is committed.
462             mIsAutoCorrectionIndicatorOn = false;
463         }
464 
465         // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
466         if (processedEvent.mCodePoint != Constants.CODE_SPACE) {
467             cancelDoubleSpacePeriodCountdown();
468         }
469 
470         Event currentEvent = processedEvent;
471         while (null != currentEvent) {
472             if (currentEvent.isConsumed()) {
473                 handleConsumedEvent(currentEvent, inputTransaction);
474             } else if (currentEvent.isFunctionalKeyEvent()) {
475                 handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId,
476                         handler);
477             } else {
478                 handleNonFunctionalEvent(currentEvent, inputTransaction, handler);
479             }
480             currentEvent = currentEvent.mNextEvent;
481         }
482         if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
483                 && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
484                 && processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
485             mLastComposedWord.deactivate();
486         if (Constants.CODE_DELETE != processedEvent.mKeyCode) {
487             mEnteredText = null;
488         }
489         mConnection.endBatchEdit();
490         return inputTransaction;
491     }
492 
onStartBatchInput(final SettingsValues settingsValues, final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler)493     public void onStartBatchInput(final SettingsValues settingsValues,
494             // TODO: remove these arguments
495             final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
496         mInputLogicHandler.onStartBatchInput();
497         handler.showGesturePreviewAndSuggestionStrip(
498                 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
499         handler.cancelUpdateSuggestionStrip();
500         ++mAutoCommitSequenceNumber;
501         mConnection.beginBatchEdit();
502         if (!mWordComposer.isComposingWord()) {
503             mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
504         } else {
505             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
506                 // If we are in the middle of a recorrection, we need to commit the recorrection
507                 // first so that we can insert the batch input at the current cursor position.
508                 resetEntireInputState(mConnection.getExpectedSelectionStart(),
509                         mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
510             } else if (mWordComposer.isSingleLetter()) {
511                 // We auto-correct the previous (typed, not gestured) string iff it's one character
512                 // long. The reason for this is, even in the middle of gesture typing, you'll still
513                 // tap one-letter words and you want them auto-corrected (typically, "i" in English
514                 // should become "I"). However for any longer word, we assume that the reason for
515                 // tapping probably is that the word you intend to type is not in the dictionary,
516                 // so we do not attempt to correct, on the assumption that if that was a dictionary
517                 // word, the user would probably have gestured instead.
518                 commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR,
519                         handler);
520             } else {
521                 commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
522             }
523         }
524         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
525         if (Character.isLetterOrDigit(codePointBeforeCursor)
526                 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
527             final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
528                     getCurrentAutoCapsState(settingsValues);
529             mSpaceState = SpaceState.PHANTOM;
530             if (!autoShiftHasBeenOverriden) {
531                 // When we change the space state, we need to update the shift state of the
532                 // keyboard unless it has been overridden manually. This is happening for example
533                 // after typing some letters and a period, then gesturing; the keyboard is not in
534                 // caps mode yet, but since a gesture is starting, it should go in caps mode,
535                 // unless the user explictly said it should not.
536                 keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues),
537                         getCurrentRecapitalizeState());
538             }
539         }
540         mConnection.endBatchEdit();
541         mWordComposer.setCapitalizedModeAtStartComposingTime(
542                 getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()));
543     }
544 
545     /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
546      * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
547      * input pointers that are held in a singleton, and to know how much to trim we rely on the
548      * results of the suggestion process that is held in mSuggestedWords.
549      * However, the suggestion process is asynchronous, and sometimes we may enter the
550      * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
551      * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
552      * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
553      * remove an unrelated number of pointers (possibly even more than are left in the input
554      * pointers, leading to a crash).
555      * To avoid that, we increase the sequence number each time we auto-commit and trim the
556      * input pointers, and we do not use any suggested words that have been generated with an
557      * earlier sequence number.
558      */
559     private int mAutoCommitSequenceNumber = 1;
onUpdateBatchInput(final SettingsValues settingsValues, final InputPointers batchPointers, final KeyboardSwitcher keyboardSwitcher)560     public void onUpdateBatchInput(final SettingsValues settingsValues,
561             final InputPointers batchPointers,
562             // TODO: remove these arguments
563             final KeyboardSwitcher keyboardSwitcher) {
564         if (settingsValues.mPhraseGestureEnabled) {
565             final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
566             // If these suggested words have been generated with out of date input pointers, then
567             // we skip auto-commit (see comments above on the mSequenceNumber member).
568             if (null != candidate
569                     && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
570                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
571                     final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2);
572                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
573                     promotePhantomSpace(settingsValues);
574                     mConnection.commitText(commitParts[0], 0);
575                     mSpaceState = SpaceState.PHANTOM;
576                     keyboardSwitcher.requestUpdatingShiftState(
577                             getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
578                     mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode(
579                             settingsValues, keyboardSwitcher.getKeyboardShiftMode()));
580                     ++mAutoCommitSequenceNumber;
581                 }
582             }
583         }
584         mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
585     }
586 
onEndBatchInput(final InputPointers batchPointers)587     public void onEndBatchInput(final InputPointers batchPointers) {
588         mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber);
589         ++mAutoCommitSequenceNumber;
590     }
591 
592     // TODO: remove this argument
onCancelBatchInput(final LatinIME.UIHandler handler)593     public void onCancelBatchInput(final LatinIME.UIHandler handler) {
594         mInputLogicHandler.onCancelBatchInput();
595         handler.showGesturePreviewAndSuggestionStrip(
596                 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
597     }
598 
599     // TODO: on the long term, this method should become private, but it will be difficult.
600     // Especially, how do we deal with InputMethodService.onDisplayCompletions?
setSuggestedWords(final SuggestedWords suggestedWords, final SettingsValues settingsValues, final LatinIME.UIHandler handler)601     public void setSuggestedWords(final SuggestedWords suggestedWords,
602             final SettingsValues settingsValues, final LatinIME.UIHandler handler) {
603         if (SuggestedWords.EMPTY != suggestedWords) {
604             final String autoCorrection;
605             if (suggestedWords.mWillAutoCorrect) {
606                 autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
607             } else {
608                 // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
609                 // because it may differ from mWordComposer.mTypedWord.
610                 autoCorrection = suggestedWords.mTypedWord;
611             }
612             mWordComposer.setAutoCorrection(autoCorrection);
613         }
614         mSuggestedWords = suggestedWords;
615         final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;
616 
617         // Put a blue underline to a word in TextView which will be auto-corrected.
618         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
619                 && mWordComposer.isComposingWord()) {
620             mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
621             final CharSequence textWithUnderline =
622                     getTextWithUnderline(mWordComposer.getTypedWord());
623             // TODO: when called from an updateSuggestionStrip() call that results from a posted
624             // message, this is called outside any batch edit. Potentially, this may result in some
625             // janky flickering of the screen, although the display speed makes it unlikely in
626             // the practice.
627             setComposingTextInternal(textWithUnderline, 1);
628         }
629     }
630 
631     /**
632      * Handle a consumed event.
633      *
634      * Consumed events represent events that have already been consumed, typically by the
635      * combining chain.
636      *
637      * @param event The event to handle.
638      * @param inputTransaction The transaction in progress.
639      */
handleConsumedEvent(final Event event, final InputTransaction inputTransaction)640     private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) {
641         // A consumed event may have text to commit and an update to the composing state, so
642         // we evaluate both. With some combiners, it's possible than an event contains both
643         // and we enter both of the following if clauses.
644         final CharSequence textToCommit = event.getTextToCommit();
645         if (!TextUtils.isEmpty(textToCommit)) {
646             mConnection.commitText(textToCommit, 1);
647             inputTransaction.setDidAffectContents();
648         }
649         if (mWordComposer.isComposingWord()) {
650             setComposingTextInternal(mWordComposer.getTypedWord(), 1);
651             inputTransaction.setDidAffectContents();
652             inputTransaction.setRequiresUpdateSuggestions();
653         }
654     }
655 
656     /**
657      * Handle a functional key event.
658      *
659      * A functional event is a special key, like delete, shift, emoji, or the settings key.
660      * Non-special keys are those that generate a single code point.
661      * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
662      * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
663      * any key that results in multiple code points like the ".com" key.
664      *
665      * @param event The event to handle.
666      * @param inputTransaction The transaction in progress.
667      */
handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)668     private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
669             // TODO: remove these arguments
670             final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
671         switch (event.mKeyCode) {
672             case Constants.CODE_DELETE:
673                 handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId);
674                 // Backspace is a functional key, but it affects the contents of the editor.
675                 inputTransaction.setDidAffectContents();
676                 break;
677             case Constants.CODE_SHIFT:
678                 performRecapitalization(inputTransaction.mSettingsValues);
679                 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
680                 if (mSuggestedWords.isPrediction()) {
681                     inputTransaction.setRequiresUpdateSuggestions();
682                 }
683                 break;
684             case Constants.CODE_CAPSLOCK:
685                 // Note: Changing keyboard to shift lock state is handled in
686                 // {@link KeyboardSwitcher#onCodeInput(int)}.
687                 break;
688             case Constants.CODE_SYMBOL_SHIFT:
689                 // Note: Calling back to the keyboard on the symbol Shift key is handled in
690                 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
691                 break;
692             case Constants.CODE_SWITCH_ALPHA_SYMBOL:
693                 // Note: Calling back to the keyboard on symbol key is handled in
694                 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
695                 break;
696             case Constants.CODE_SETTINGS:
697                 onSettingsKeyPressed();
698                 break;
699             case Constants.CODE_SHORTCUT:
700                 // We need to switch to the shortcut IME. This is handled by LatinIME since the
701                 // input logic has no business with IME switching.
702                 break;
703             case Constants.CODE_ACTION_NEXT:
704                 performEditorAction(EditorInfo.IME_ACTION_NEXT);
705                 break;
706             case Constants.CODE_ACTION_PREVIOUS:
707                 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
708                 break;
709             case Constants.CODE_LANGUAGE_SWITCH:
710                 handleLanguageSwitchKey();
711                 break;
712             case Constants.CODE_EMOJI:
713                 // Note: Switching emoji keyboard is being handled in
714                 // {@link KeyboardState#onCodeInput(int,int)}.
715                 break;
716             case Constants.CODE_ALPHA_FROM_EMOJI:
717                 // Note: Switching back from Emoji keyboard to the main keyboard is being
718                 // handled in {@link KeyboardState#onCodeInput(int,int)}.
719                 break;
720             case Constants.CODE_SHIFT_ENTER:
721                 // TODO: remove this object
722                 final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
723                         event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
724                 handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
725                 // Shift + Enter is treated as a functional key but it results in adding a new
726                 // line, so that does affect the contents of the editor.
727                 inputTransaction.setDidAffectContents();
728                 break;
729             default:
730                 throw new RuntimeException("Unknown key code : " + event.mKeyCode);
731         }
732     }
733 
734     /**
735      * Handle an event that is not a functional event.
736      *
737      * These events are generally events that cause input, but in some cases they may do other
738      * things like trigger an editor action.
739      *
740      * @param event The event to handle.
741      * @param inputTransaction The transaction in progress.
742      */
handleNonFunctionalEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)743     private void handleNonFunctionalEvent(final Event event,
744             final InputTransaction inputTransaction,
745             // TODO: remove this argument
746             final LatinIME.UIHandler handler) {
747         inputTransaction.setDidAffectContents();
748         switch (event.mCodePoint) {
749             case Constants.CODE_ENTER:
750                 final EditorInfo editorInfo = getCurrentInputEditorInfo();
751                 final int imeOptionsActionId =
752                         InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
753                 if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
754                     // Either we have an actionLabel and we should performEditorAction with
755                     // actionId regardless of its value.
756                     performEditorAction(editorInfo.actionId);
757                 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
758                     // We didn't have an actionLabel, but we had another action to execute.
759                     // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
760                     // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
761                     // means there should be an action and the app didn't bother to set a specific
762                     // code for it - presumably it only handles one. It does not have to be treated
763                     // in any specific way: anything that is not IME_ACTION_NONE should be sent to
764                     // performEditorAction.
765                     performEditorAction(imeOptionsActionId);
766                 } else {
767                     // No action label, and the action from imeOptions is NONE: this is a regular
768                     // enter key that should input a carriage return.
769                     handleNonSpecialCharacterEvent(event, inputTransaction, handler);
770                 }
771                 break;
772             default:
773                 handleNonSpecialCharacterEvent(event, inputTransaction, handler);
774                 break;
775         }
776     }
777 
778     /**
779      * Handle inputting a code point to the editor.
780      *
781      * Non-special keys are those that generate a single code point.
782      * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
783      * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
784      * any key that results in multiple code points like the ".com" key.
785      *
786      * @param event The event to handle.
787      * @param inputTransaction The transaction in progress.
788      */
handleNonSpecialCharacterEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)789     private void handleNonSpecialCharacterEvent(final Event event,
790             final InputTransaction inputTransaction,
791             // TODO: remove this argument
792             final LatinIME.UIHandler handler) {
793         if (!mWordComposer.isComposingWord()) {
794             mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
795             // In case the "add to dictionary" hint was still displayed.
796             // TODO: Do we really need to check if we have composing text here?
797             if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) {
798                 mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
799                 mTextDecorator.reset();
800             }
801         }
802 
803         final int codePoint = event.mCodePoint;
804         mSpaceState = SpaceState.NONE;
805         if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
806                 || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
807             handleSeparatorEvent(event, inputTransaction, handler);
808         } else {
809             if (SpaceState.PHANTOM == inputTransaction.mSpaceState) {
810                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
811                     // If we are in the middle of a recorrection, we need to commit the recorrection
812                     // first so that we can insert the character at the current cursor position.
813                     resetEntireInputState(mConnection.getExpectedSelectionStart(),
814                             mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
815                 } else {
816                     commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR);
817                 }
818             }
819             handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction);
820         }
821     }
822 
823     /**
824      * Handle a non-separator.
825      * @param event The event to handle.
826      * @param settingsValues The current settings values.
827      * @param inputTransaction The transaction in progress.
828      */
handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues, final InputTransaction inputTransaction)829     private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues,
830             final InputTransaction inputTransaction) {
831         final int codePoint = event.mCodePoint;
832         // TODO: refactor this method to stop flipping isComposingWord around all the time, and
833         // make it shorter (possibly cut into several pieces). Also factor
834         // handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
835         // not the same.
836         boolean isComposingWord = mWordComposer.isComposingWord();
837 
838         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
839         // See onStartBatchInput() to see how to do it.
840         if (SpaceState.PHANTOM == inputTransaction.mSpaceState
841                 && !settingsValues.isWordConnector(codePoint)) {
842             if (isComposingWord) {
843                 // Sanity check
844                 throw new RuntimeException("Should not be composing here");
845             }
846             promotePhantomSpace(settingsValues);
847         }
848 
849         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
850             // If we are in the middle of a recorrection, we need to commit the recorrection
851             // first so that we can insert the character at the current cursor position.
852             resetEntireInputState(mConnection.getExpectedSelectionStart(),
853                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
854             isComposingWord = false;
855         }
856         // We want to find out whether to start composing a new word with this character. If so,
857         // we need to reset the composing state and switch isComposingWord. The order of the
858         // tests is important for good performance.
859         // We only start composing if we're not already composing.
860         if (!isComposingWord
861         // We only start composing if this is a word code point. Essentially that means it's a
862         // a letter or a word connector.
863                 && settingsValues.isWordCodePoint(codePoint)
864         // We never go into composing state if suggestions are not requested.
865                 && settingsValues.needsToLookupSuggestions() &&
866         // In languages with spaces, we only start composing a word when we are not already
867         // touching a word. In languages without spaces, the above conditions are sufficient.
868                 (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)
869                         || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) {
870             // Reset entirely the composing state anyway, then start composing a new word unless
871             // the character is a word connector. The idea here is, word connectors are not
872             // separators and they should be treated as normal characters, except in the first
873             // position where they should not start composing a word.
874             isComposingWord = !settingsValues.mSpacingAndPunctuations.isWordConnector(codePoint);
875             // Here we don't need to reset the last composed word. It will be reset
876             // when we commit this one, if we ever do; if on the other hand we backspace
877             // it entirely and resume suggestions on the previous word, we'd like to still
878             // have touch coordinates for it.
879             resetComposingState(false /* alsoResetLastComposedWord */);
880         }
881         if (isComposingWord) {
882             mWordComposer.applyProcessedEvent(event);
883             // If it's the first letter, make note of auto-caps state
884             if (mWordComposer.isSingleLetter()) {
885                 mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
886             }
887             setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
888         } else {
889             final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
890                     inputTransaction);
891 
892             if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
893                 mSpaceState = SpaceState.WEAK;
894             } else {
895                 sendKeyCodePoint(settingsValues, codePoint);
896             }
897         }
898         inputTransaction.setRequiresUpdateSuggestions();
899     }
900 
901     /**
902      * Handle input of a separator code point.
903      * @param event The event to handle.
904      * @param inputTransaction The transaction in progress.
905      */
handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)906     private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction,
907             // TODO: remove this argument
908             final LatinIME.UIHandler handler) {
909         final int codePoint = event.mCodePoint;
910         final SettingsValues settingsValues = inputTransaction.mSettingsValues;
911         final boolean wasComposingWord = mWordComposer.isComposingWord();
912         // We avoid sending spaces in languages without spaces if we were composing.
913         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
914                 && !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
915                 && wasComposingWord;
916         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
917             // If we are in the middle of a recorrection, we need to commit the recorrection
918             // first so that we can insert the separator at the current cursor position.
919             resetEntireInputState(mConnection.getExpectedSelectionStart(),
920                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
921         }
922         // isComposingWord() may have changed since we stored wasComposing
923         if (mWordComposer.isComposingWord()) {
924             if (settingsValues.mAutoCorrectionEnabledPerUserSettings) {
925                 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
926                         : StringUtils.newSingleCodePointString(codePoint);
927                 commitCurrentAutoCorrection(settingsValues, separator, handler);
928                 inputTransaction.setDidAutoCorrect();
929             } else {
930                 commitTyped(settingsValues,
931                         StringUtils.newSingleCodePointString(codePoint));
932             }
933         }
934 
935         final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
936                 inputTransaction);
937 
938         final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
939                 && mConnection.isInsideDoubleQuoteOrAfterDigit();
940 
941         final boolean needsPrecedingSpace;
942         if (SpaceState.PHANTOM != inputTransaction.mSpaceState) {
943             needsPrecedingSpace = false;
944         } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
945             // Double quotes behave like they are usually preceded by space iff we are
946             // not inside a double quote or after a digit.
947             needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit;
948         } else if (settingsValues.mSpacingAndPunctuations.isClusteringSymbol(codePoint)
949                 && settingsValues.mSpacingAndPunctuations.isClusteringSymbol(
950                         mConnection.getCodePointBeforeCursor())) {
951             needsPrecedingSpace = false;
952         } else {
953             needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint);
954         }
955 
956         if (needsPrecedingSpace) {
957             promotePhantomSpace(settingsValues);
958         }
959 
960         if (tryPerformDoubleSpacePeriod(event, inputTransaction)) {
961             mSpaceState = SpaceState.DOUBLE;
962             inputTransaction.setRequiresUpdateSuggestions();
963         } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
964             mSpaceState = SpaceState.SWAP_PUNCTUATION;
965             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
966         } else if (Constants.CODE_SPACE == codePoint) {
967             if (!mSuggestedWords.isPunctuationSuggestions()) {
968                 mSpaceState = SpaceState.WEAK;
969             }
970 
971             startDoubleSpacePeriodCountdown(inputTransaction);
972             if (wasComposingWord || mSuggestedWords.isEmpty()) {
973                 inputTransaction.setRequiresUpdateSuggestions();
974             }
975 
976             if (!shouldAvoidSendingCode) {
977                 sendKeyCodePoint(settingsValues, codePoint);
978             }
979         } else {
980             if ((SpaceState.PHANTOM == inputTransaction.mSpaceState
981                     && settingsValues.isUsuallyFollowedBySpace(codePoint))
982                     || (Constants.CODE_DOUBLE_QUOTE == codePoint
983                             && isInsideDoubleQuoteOrAfterDigit)) {
984                 // If we are in phantom space state, and the user presses a separator, we want to
985                 // stay in phantom space state so that the next keypress has a chance to add the
986                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
987                 // then insert a comma and go on to typing the next word, I want the space to be
988                 // inserted automatically before the next word, the same way it is when I don't
989                 // input the comma. A double quote behaves like it's usually followed by space if
990                 // we're inside a double quote.
991                 // The case is a little different if the separator is a space stripper. Such a
992                 // separator does not normally need a space on the right (that's the difference
993                 // between swappers and strippers), so we should not stay in phantom space state if
994                 // the separator is a stripper. Hence the additional test above.
995                 mSpaceState = SpaceState.PHANTOM;
996             }
997 
998             sendKeyCodePoint(settingsValues, codePoint);
999 
1000             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
1001             // already displayed or not, so it's okay.
1002             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
1003         }
1004 
1005         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
1006     }
1007 
1008     /**
1009      * Handle a press on the backspace key.
1010      * @param event The event to handle.
1011      * @param inputTransaction The transaction in progress.
1012      */
handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, final int currentKeyboardScriptId)1013     private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction,
1014             // TODO: remove this argument, put it into settingsValues
1015             final int currentKeyboardScriptId) {
1016         mSpaceState = SpaceState.NONE;
1017         mDeleteCount++;
1018 
1019         // In many cases after backspace, we need to update the shift state. Normally we need
1020         // to do this right away to avoid the shift state being out of date in case the user types
1021         // backspace then some other character very fast. However, in the case of backspace key
1022         // repeat, this can lead to flashiness when the cursor flies over positions where the
1023         // shift state should be updated, so if this is a key repeat, we update after a small delay.
1024         // Then again, even in the case of a key repeat, if the cursor is at start of text, it
1025         // can't go any further back, so we can update right away even if it's a key repeat.
1026         final int shiftUpdateKind =
1027                 event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
1028                 ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
1029         inputTransaction.requireShiftUpdate(shiftUpdateKind);
1030 
1031         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
1032             // If we are in the middle of a recorrection, we need to commit the recorrection
1033             // first so that we can remove the character at the current cursor position.
1034             resetEntireInputState(mConnection.getExpectedSelectionStart(),
1035                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
1036             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
1037         }
1038         if (mWordComposer.isComposingWord()) {
1039             if (mWordComposer.isBatchMode()) {
1040                 final String rejectedSuggestion = mWordComposer.getTypedWord();
1041                 mWordComposer.reset();
1042                 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
1043                 if (!TextUtils.isEmpty(rejectedSuggestion)) {
1044                     mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion);
1045                 }
1046             } else {
1047                 mWordComposer.applyProcessedEvent(event);
1048             }
1049             if (mWordComposer.isComposingWord()) {
1050                 setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
1051             } else {
1052                 mConnection.commitText("", 1);
1053             }
1054             inputTransaction.setRequiresUpdateSuggestions();
1055         } else {
1056             if (mLastComposedWord.canRevertCommit()) {
1057                 revertCommit(inputTransaction, inputTransaction.mSettingsValues);
1058                 return;
1059             }
1060             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
1061                 // Cancel multi-character input: remove the text we just entered.
1062                 // This is triggered on backspace after a key that inputs multiple characters,
1063                 // like the smiley key or the .com key.
1064                 mConnection.deleteSurroundingText(mEnteredText.length(), 0);
1065                 mEnteredText = null;
1066                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
1067                 // In addition we know that spaceState is false, and that we should not be
1068                 // reverting any autocorrect at this point. So we can safely return.
1069                 return;
1070             }
1071             if (SpaceState.DOUBLE == inputTransaction.mSpaceState) {
1072                 cancelDoubleSpacePeriodCountdown();
1073                 if (mConnection.revertDoubleSpacePeriod()) {
1074                     // No need to reset mSpaceState, it has already be done (that's why we
1075                     // receive it as a parameter)
1076                     inputTransaction.setRequiresUpdateSuggestions();
1077                     mWordComposer.setCapitalizedModeAtStartComposingTime(
1078                             WordComposer.CAPS_MODE_OFF);
1079                     return;
1080                 }
1081             } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
1082                 if (mConnection.revertSwapPunctuation()) {
1083                     // Likewise
1084                     return;
1085                 }
1086             }
1087 
1088             // No cancelling of commit/double space/swap: we have a regular backspace.
1089             // We should backspace one char and restart suggestion if at the end of a word.
1090             if (mConnection.hasSelection()) {
1091                 // If there is a selection, remove it.
1092                 final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
1093                         - mConnection.getExpectedSelectionStart();
1094                 mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
1095                         mConnection.getExpectedSelectionEnd());
1096                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
1097             } else {
1098                 // There is no selection, just delete one character.
1099                 if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
1100                     // This should never happen.
1101                     Log.e(TAG, "Backspace when we don't know the selection position");
1102                 }
1103                 if (inputTransaction.mSettingsValues.isBeforeJellyBean() ||
1104                         inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) {
1105                     // There are two possible reasons to send a key event: either the field has
1106                     // type TYPE_NULL, in which case the keyboard should send events, or we are
1107                     // running in backward compatibility mode. Before Jelly bean, the keyboard
1108                     // would simulate a hardware keyboard event on pressing enter or delete. This
1109                     // is bad for many reasons (there are race conditions with commits) but some
1110                     // applications are relying on this behavior so we continue to support it for
1111                     // older apps, so we retain this behavior if the app has target SDK < JellyBean.
1112                     sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
1113                     if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
1114                         sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
1115                     }
1116                 } else {
1117                     final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1118                     if (codePointBeforeCursor == Constants.NOT_A_CODE) {
1119                         // HACK for backward compatibility with broken apps that haven't realized
1120                         // yet that hardware keyboards are not the only way of inputting text.
1121                         // Nothing to delete before the cursor. We should not do anything, but many
1122                         // broken apps expect something to happen in this case so that they can
1123                         // catch it and have their broken interface react. If you need the keyboard
1124                         // to do this, you're doing it wrong -- please fix your app.
1125                         mConnection.deleteSurroundingText(1, 0);
1126                         return;
1127                     }
1128                     final int lengthToDelete =
1129                             Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
1130                     mConnection.deleteSurroundingText(lengthToDelete, 0);
1131                     if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
1132                         final int codePointBeforeCursorToDeleteAgain =
1133                                 mConnection.getCodePointBeforeCursor();
1134                         if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
1135                             final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
1136                                     codePointBeforeCursorToDeleteAgain) ? 2 : 1;
1137                             mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
1138                         }
1139                     }
1140                 }
1141             }
1142             if (inputTransaction.mSettingsValues
1143                     .isSuggestionsEnabledPerUserSettings()
1144                     && inputTransaction.mSettingsValues.mSpacingAndPunctuations
1145                             .mCurrentLanguageHasSpaces
1146                     && !mConnection.isCursorFollowedByWordCharacter(
1147                             inputTransaction.mSettingsValues.mSpacingAndPunctuations)) {
1148                 restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues,
1149                         true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId);
1150             }
1151         }
1152     }
1153 
1154     /**
1155      * Handle a press on the language switch key (the "globe key")
1156      */
handleLanguageSwitchKey()1157     private void handleLanguageSwitchKey() {
1158         mLatinIME.switchToNextSubtype();
1159     }
1160 
1161     /**
1162      * Swap a space with a space-swapping punctuation sign.
1163      *
1164      * This method will check that there are two characters before the cursor and that the first
1165      * one is a space before it does the actual swapping.
1166      * @param event The event to handle.
1167      * @param inputTransaction The transaction in progress.
1168      * @return true if the swap has been performed, false if it was prevented by preliminary checks.
1169      */
trySwapSwapperAndSpace(final Event event, final InputTransaction inputTransaction)1170     private boolean trySwapSwapperAndSpace(final Event event,
1171             final InputTransaction inputTransaction) {
1172         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1173         if (Constants.CODE_SPACE != codePointBeforeCursor) {
1174             return false;
1175         }
1176         mConnection.deleteSurroundingText(1, 0);
1177         final String text = event.getTextToCommit() + " ";
1178         mConnection.commitText(text, 1);
1179         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
1180         return true;
1181     }
1182 
1183     /*
1184      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
1185      * @param event The event to handle.
1186      * @param inputTransaction The transaction in progress.
1187      * @return whether we should swap the space instead of removing it.
1188      */
tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event, final InputTransaction inputTransaction)1189     private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event,
1190             final InputTransaction inputTransaction) {
1191         final int codePoint = event.mCodePoint;
1192         final boolean isFromSuggestionStrip = event.isSuggestionStripPress();
1193         if (Constants.CODE_ENTER == codePoint &&
1194                 SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
1195             mConnection.removeTrailingSpace();
1196             return false;
1197         }
1198         if ((SpaceState.WEAK == inputTransaction.mSpaceState
1199                 || SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState)
1200                 && isFromSuggestionStrip) {
1201             if (inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(codePoint)) {
1202                 return false;
1203             }
1204             if (inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) {
1205                 return true;
1206             }
1207             mConnection.removeTrailingSpace();
1208         }
1209         return false;
1210     }
1211 
startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction)1212     public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) {
1213         mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp;
1214     }
1215 
cancelDoubleSpacePeriodCountdown()1216     public void cancelDoubleSpacePeriodCountdown() {
1217         mDoubleSpacePeriodCountdownStart = 0;
1218     }
1219 
isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction)1220     public boolean isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction) {
1221         return inputTransaction.mTimestamp - mDoubleSpacePeriodCountdownStart
1222                 < inputTransaction.mSettingsValues.mDoubleSpacePeriodTimeout;
1223     }
1224 
1225     /**
1226      * Apply the double-space-to-period transformation if applicable.
1227      *
1228      * The double-space-to-period transformation means that we replace two spaces with a
1229      * period-space sequence of characters. This typically happens when the user presses space
1230      * twice in a row quickly.
1231      * This method will check that the double-space-to-period is active in settings, that the
1232      * two spaces have been input close enough together, that the typed character is a space
1233      * and that the previous character allows for the transformation to take place. If all of
1234      * these conditions are fulfilled, this method applies the transformation and returns true.
1235      * Otherwise, it does nothing and returns false.
1236      *
1237      * @param event The event to handle.
1238      * @param inputTransaction The transaction in progress.
1239      * @return true if we applied the double-space-to-period transformation, false otherwise.
1240      */
tryPerformDoubleSpacePeriod(final Event event, final InputTransaction inputTransaction)1241     private boolean tryPerformDoubleSpacePeriod(final Event event,
1242             final InputTransaction inputTransaction) {
1243         // Check the setting, the typed character and the countdown. If any of the conditions is
1244         // not fulfilled, return false.
1245         if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod
1246                 || Constants.CODE_SPACE != event.mCodePoint
1247                 || !isDoubleSpacePeriodCountdownActive(inputTransaction)) {
1248             return false;
1249         }
1250         // We only do this when we see one space and an accepted code point before the cursor.
1251         // The code point may be a surrogate pair but the space may not, so we need 3 chars.
1252         final CharSequence lastTwo = mConnection.getTextBeforeCursor(3, 0);
1253         if (null == lastTwo) return false;
1254         final int length = lastTwo.length();
1255         if (length < 2) return false;
1256         if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false;
1257         // We know there is a space in pos -1, and we have at least two chars. If we have only two
1258         // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine.
1259         final int firstCodePoint =
1260                 Character.isSurrogatePair(lastTwo.charAt(0), lastTwo.charAt(1)) ?
1261                         Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2);
1262         if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
1263             cancelDoubleSpacePeriodCountdown();
1264             mConnection.deleteSurroundingText(1, 0);
1265             final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations
1266                     .mSentenceSeparatorAndSpace;
1267             mConnection.commitText(textToInsert, 1);
1268             inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
1269             inputTransaction.setRequiresUpdateSuggestions();
1270             return true;
1271         }
1272         return false;
1273     }
1274 
1275     /**
1276      * Returns whether this code point can be followed by the double-space-to-period transformation.
1277      *
1278      * See #maybeDoubleSpaceToPeriod for details.
1279      * Generally, most word characters can be followed by the double-space-to-period transformation,
1280      * while most punctuation can't. Some punctuation however does allow for this to take place
1281      * after them, like the closing parenthesis for example.
1282      *
1283      * @param codePoint the code point after which we may want to apply the transformation
1284      * @return whether it's fine to apply the transformation after this code point.
1285      */
canBeFollowedByDoubleSpacePeriod(final int codePoint)1286     private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
1287         // TODO: This should probably be a blacklist rather than a whitelist.
1288         // TODO: This should probably be language-dependant...
1289         return Character.isLetterOrDigit(codePoint)
1290                 || codePoint == Constants.CODE_SINGLE_QUOTE
1291                 || codePoint == Constants.CODE_DOUBLE_QUOTE
1292                 || codePoint == Constants.CODE_CLOSING_PARENTHESIS
1293                 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
1294                 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
1295                 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
1296                 || codePoint == Constants.CODE_PLUS
1297                 || codePoint == Constants.CODE_PERCENT
1298                 || Character.getType(codePoint) == Character.OTHER_SYMBOL;
1299     }
1300 
1301     /**
1302      * Performs a recapitalization event.
1303      * @param settingsValues The current settings values.
1304      */
performRecapitalization(final SettingsValues settingsValues)1305     private void performRecapitalization(final SettingsValues settingsValues) {
1306         if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) {
1307             return; // No selection or recapitalize is disabled for now
1308         }
1309         final int selectionStart = mConnection.getExpectedSelectionStart();
1310         final int selectionEnd = mConnection.getExpectedSelectionEnd();
1311         final int numCharsSelected = selectionEnd - selectionStart;
1312         if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) {
1313             // We bail out if we have too many characters for performance reasons. We don't want
1314             // to suck possibly multiple-megabyte data.
1315             return;
1316         }
1317         // If we have a recapitalize in progress, use it; otherwise, start a new one.
1318         if (!mRecapitalizeStatus.isStarted()
1319                 || !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) {
1320             final CharSequence selectedText =
1321                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
1322             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
1323             mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(),
1324                     settingsValues.mLocale,
1325                     settingsValues.mSpacingAndPunctuations.mSortedWordSeparators);
1326             // We trim leading and trailing whitespace.
1327             mRecapitalizeStatus.trim();
1328         }
1329         mConnection.finishComposingText();
1330         mRecapitalizeStatus.rotate();
1331         mConnection.setSelection(selectionEnd, selectionEnd);
1332         mConnection.deleteSurroundingText(numCharsSelected, 0);
1333         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
1334         mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
1335                 mRecapitalizeStatus.getNewCursorEnd());
1336     }
1337 
performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, final String suggestion, final PrevWordsInfo prevWordsInfo)1338     private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues,
1339             final String suggestion, final PrevWordsInfo prevWordsInfo) {
1340         // If correction is not enabled, we don't add words to the user history dictionary.
1341         // That's to avoid unintended additions in some sensitive fields, or fields that
1342         // expect to receive non-words.
1343         if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return;
1344 
1345         if (TextUtils.isEmpty(suggestion)) return;
1346         final boolean wasAutoCapitalized =
1347                 mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps();
1348         final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
1349                 System.currentTimeMillis());
1350         mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized,
1351                 prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive);
1352     }
1353 
performUpdateSuggestionStripSync(final SettingsValues settingsValues, final int inputStyle)1354     public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
1355             final int inputStyle) {
1356         // Check if we have a suggestion engine attached.
1357         if (!settingsValues.needsToLookupSuggestions()) {
1358             if (mWordComposer.isComposingWord()) {
1359                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
1360                         + "requested!");
1361             }
1362             // Clear the suggestions strip.
1363             mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY);
1364             return;
1365         }
1366 
1367         if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) {
1368             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
1369             return;
1370         }
1371 
1372         final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>();
1373         mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
1374                 new OnGetSuggestedWordsCallback() {
1375                     @Override
1376                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
1377                         final String typedWord = mWordComposer.getTypedWord();
1378                         // Show new suggestions if we have at least one. Otherwise keep the old
1379                         // suggestions with the new typed word. Exception: if the length of the
1380                         // typed word is <= 1 (after a deletion typically) we clear old suggestions.
1381                         if (suggestedWords.size() > 1 || typedWord.length() <= 1) {
1382                             holder.set(suggestedWords);
1383                         } else {
1384                             holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords));
1385                         }
1386                     }
1387                 }
1388         );
1389 
1390         // This line may cause the current thread to wait.
1391         final SuggestedWords suggestedWords = holder.get(null,
1392                 Constants.GET_SUGGESTED_WORDS_TIMEOUT);
1393         if (suggestedWords != null) {
1394             mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords);
1395         }
1396     }
1397 
1398     /**
1399      * Check if the cursor is touching a word. If so, restart suggestions on this word, else
1400      * do nothing.
1401      *
1402      * @param settingsValues the current values of the settings.
1403      * @param shouldIncludeResumedWordInSuggestions whether to include the word on which we resume
1404      *   suggestions in the suggestion list.
1405      */
1406     // TODO: make this private.
restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, final boolean shouldIncludeResumedWordInSuggestions, final int currentKeyboardScriptId)1407     public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues,
1408             final boolean shouldIncludeResumedWordInSuggestions,
1409             // TODO: remove this argument, put it into settingsValues
1410             final int currentKeyboardScriptId) {
1411         // HACK: We may want to special-case some apps that exhibit bad behavior in case of
1412         // recorrection. This is a temporary, stopgap measure that will be removed later.
1413         // TODO: remove this.
1414         if (settingsValues.isBrokenByRecorrection()
1415         // Recorrection is not supported in languages without spaces because we don't know
1416         // how to segment them yet.
1417                 || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
1418         // If no suggestions are requested, don't try restarting suggestions.
1419                 || !settingsValues.needsToLookupSuggestions()
1420         // If we are currently in a batch input, we must not resume suggestions, or the result
1421         // of the batch input will replace the new composition. This may happen in the corner case
1422         // that the app moves the cursor on its own accord during a batch input.
1423                 || mInputLogicHandler.isInBatchInput()
1424         // If the cursor is not touching a word, or if there is a selection, return right away.
1425                 || mConnection.hasSelection()
1426         // If we don't know the cursor location, return.
1427                 || mConnection.getExpectedSelectionStart() < 0) {
1428             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
1429             return;
1430         }
1431         final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
1432         if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) {
1433             // Show predictions.
1434             mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF);
1435             mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION);
1436             return;
1437         }
1438         final TextRange range = mConnection.getWordRangeAtCursor(
1439                 settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId);
1440         if (null == range) return; // Happens if we don't have an input connection at all
1441         if (range.length() <= 0) {
1442             // Race condition, or touching a word in a non-supported script.
1443             mLatinIME.setNeutralSuggestionStrip();
1444             return;
1445         }
1446         // If for some strange reason (editor bug or so) we measure the text before the cursor as
1447         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
1448         if (range.mHasUrlSpans) return; // If there are links, we don't resume suggestions. Making
1449         // edits to a linkified text through batch commands would ruin the URL spans, and unless
1450         // we take very complicated steps to preserve the whole link, we can't do things right so
1451         // we just do not resume because it's safer.
1452         final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
1453         if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return;
1454         final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
1455         final String typedWord = range.mWord.toString();
1456         if (shouldIncludeResumedWordInSuggestions) {
1457             suggestions.add(new SuggestedWordInfo(typedWord,
1458                     SuggestedWords.MAX_SUGGESTIONS + 1,
1459                     SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
1460                     SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
1461                     SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
1462         }
1463         if (!isResumableWord(settingsValues, typedWord)) {
1464             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
1465             return;
1466         }
1467         int i = 0;
1468         for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) {
1469             for (final String s : span.getSuggestions()) {
1470                 ++i;
1471                 if (!TextUtils.equals(s, typedWord)) {
1472                     suggestions.add(new SuggestedWordInfo(s,
1473                             SuggestedWords.MAX_SUGGESTIONS - i,
1474                             SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED,
1475                             SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
1476                             SuggestedWordInfo.NOT_A_CONFIDENCE
1477                                     /* autoCommitFirstWordConfidence */));
1478                 }
1479             }
1480         }
1481         final int[] codePoints = StringUtils.toCodePointArray(typedWord);
1482         // We want the previous word for suggestion. If we have chars in the word
1483         // before the cursor, then we want the word before that, hence 2; otherwise,
1484         // we want the word immediately before the cursor, hence 1.
1485         final PrevWordsInfo prevWordsInfo = getPrevWordsInfoFromNthPreviousWordForSuggestion(
1486                 settingsValues.mSpacingAndPunctuations,
1487                 0 == numberOfCharsInWordBeforeCursor ? 1 : 2);
1488         mWordComposer.setComposingWord(codePoints,
1489                 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
1490         mWordComposer.setCursorPositionWithinWord(
1491                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
1492         mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug();
1493         mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor,
1494                 expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor());
1495         if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) {
1496             // If there weren't any suggestion spans on this word, suggestions#size() will be 1
1497             // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we
1498             // have no useful suggestions, so we will try to compute some for it instead.
1499             mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING,
1500                     SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
1501                         @Override
1502                         public void onGetSuggestedWords(
1503                                 final SuggestedWords suggestedWordsIncludingTypedWord) {
1504                             final SuggestedWords suggestedWords;
1505                             if (suggestedWordsIncludingTypedWord.size() > 1
1506                                     && !shouldIncludeResumedWordInSuggestions) {
1507                                 // We were able to compute new suggestions for this word.
1508                                 // Remove the typed word, since we don't want to display it in this
1509                                 // case. The #getSuggestedWordsExcludingTypedWordForRecorrection()
1510                                 // method sets willAutoCorrect to false.
1511                                 suggestedWords = suggestedWordsIncludingTypedWord
1512                                         .getSuggestedWordsExcludingTypedWordForRecorrection();
1513                             } else {
1514                                 // No saved suggestions, and we were unable to compute any good one
1515                                 // either. Rather than displaying an empty suggestion strip, we'll
1516                                 // display the original word alone in the middle.
1517                                 // Since there is only one word, willAutoCorrect is false.
1518                                 suggestedWords = suggestedWordsIncludingTypedWord;
1519                             }
1520                             mIsAutoCorrectionIndicatorOn = false;
1521                             mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
1522                         }});
1523         } else {
1524             // We found suggestion spans in the word. We'll create the SuggestedWords out of
1525             // them, and make willAutoCorrect false. We make typedWordValid false, because the
1526             // color of the word in the suggestion strip changes according to this parameter,
1527             // and false gives the correct color.
1528             final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
1529                     null /* rawSuggestions */, typedWord, false /* typedWordValid */,
1530                     false /* willAutoCorrect */, false /* isObsoleteSuggestions */,
1531                     SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER);
1532             mIsAutoCorrectionIndicatorOn = false;
1533             mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
1534         }
1535     }
1536 
1537     /**
1538      * Reverts a previous commit with auto-correction.
1539      *
1540      * This is triggered upon pressing backspace just after a commit with auto-correction.
1541      *
1542      * @param inputTransaction The transaction in progress.
1543      * @param settingsValues the current values of the settings.
1544      */
revertCommit(final InputTransaction inputTransaction, final SettingsValues settingsValues)1545     private void revertCommit(final InputTransaction inputTransaction,
1546             final SettingsValues settingsValues) {
1547         final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord;
1548         final String originallyTypedWordString =
1549                 originallyTypedWord != null ? originallyTypedWord.toString() : "";
1550         final CharSequence committedWord = mLastComposedWord.mCommittedWord;
1551         final String committedWordString = committedWord.toString();
1552         final int cancelLength = committedWord.length();
1553         final String separatorString = mLastComposedWord.mSeparatorString;
1554         // We want java chars, not codepoints for the following.
1555         final int separatorLength = separatorString.length();
1556         // TODO: should we check our saved separator against the actual contents of the text view?
1557         final int deleteLength = cancelLength + separatorLength;
1558         if (DebugFlags.DEBUG_ENABLED) {
1559             if (mWordComposer.isComposingWord()) {
1560                 throw new RuntimeException("revertCommit, but we are composing a word");
1561             }
1562             final CharSequence wordBeforeCursor =
1563                     mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength);
1564             if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
1565                 throw new RuntimeException("revertCommit check failed: we thought we were "
1566                         + "reverting \"" + committedWord
1567                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
1568             }
1569         }
1570         mConnection.deleteSurroundingText(deleteLength, 0);
1571         if (!TextUtils.isEmpty(committedWord)) {
1572             mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString);
1573         }
1574         final String stringToCommit = originallyTypedWord + separatorString;
1575         final SpannableString textToCommit = new SpannableString(stringToCommit);
1576         if (committedWord instanceof SpannableString) {
1577             final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord;
1578             final Object[] spans = committedWordWithSuggestionSpans.getSpans(0,
1579                     committedWord.length(), Object.class);
1580             final int lastCharIndex = textToCommit.length() - 1;
1581             // We will collect all suggestions in the following array.
1582             final ArrayList<String> suggestions = new ArrayList<>();
1583             // First, add the committed word to the list of suggestions.
1584             suggestions.add(committedWordString);
1585             for (final Object span : spans) {
1586                 // If this is a suggestion span, we check that the locale is the right one, and
1587                 // that the word is not the committed word. That should mostly be the case.
1588                 // Given this, we add it to the list of suggestions, otherwise we discard it.
1589                 if (span instanceof SuggestionSpan) {
1590                     final SuggestionSpan suggestionSpan = (SuggestionSpan)span;
1591                     if (!suggestionSpan.getLocale().equals(
1592                             inputTransaction.mSettingsValues.mLocale.toString())) {
1593                         continue;
1594                     }
1595                     for (final String suggestion : suggestionSpan.getSuggestions()) {
1596                         if (!suggestion.equals(committedWordString)) {
1597                             suggestions.add(suggestion);
1598                         }
1599                     }
1600                 } else {
1601                     // If this is not a suggestion span, we just add it as is.
1602                     textToCommit.setSpan(span, 0 /* start */, lastCharIndex /* end */,
1603                             committedWordWithSuggestionSpans.getSpanFlags(span));
1604                 }
1605             }
1606             // Add the suggestion list to the list of suggestions.
1607             textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale,
1608                     suggestions.toArray(new String[suggestions.size()]), 0 /* flags */),
1609                     0 /* start */, lastCharIndex /* end */, 0 /* flags */);
1610         }
1611 
1612         final boolean shouldShowAddToDictionaryForTypedWord =
1613                 shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues);
1614 
1615         if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
1616             // For languages with spaces, we revert to the typed string, but the cursor is still
1617             // after the separator so we don't resume suggestions. If the user wants to correct
1618             // the word, they have to press backspace again.
1619             if (shouldShowAddToDictionaryForTypedWord) {
1620                 mConnection.commitTextWithBackgroundColor(textToCommit, 1,
1621                         settingsValues.mTextHighlightColorForAddToDictionaryIndicator,
1622                         originallyTypedWordString.length());
1623             } else {
1624                 mConnection.commitText(textToCommit, 1);
1625             }
1626         } else {
1627             // For languages without spaces, we revert the typed string but the cursor is flush
1628             // with the typed word, so we need to resume suggestions right away.
1629             final int[] codePoints = StringUtils.toCodePointArray(stringToCommit);
1630             mWordComposer.setComposingWord(codePoints,
1631                     mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
1632             if (shouldShowAddToDictionaryForTypedWord) {
1633                 setComposingTextInternalWithBackgroundColor(textToCommit, 1,
1634                         settingsValues.mTextHighlightColorForAddToDictionaryIndicator,
1635                         originallyTypedWordString.length());
1636             } else {
1637                 setComposingTextInternal(textToCommit, 1);
1638             }
1639         }
1640         // Don't restart suggestion yet. We'll restart if the user deletes the separator.
1641         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1642 
1643         if (shouldShowAddToDictionaryForTypedWord) {
1644             // Due to the API limitation as of L, we cannot reliably retrieve the reverted text
1645             // when the separator causes line breaking. Until this API limitation is addressed in
1646             // the framework, show the indicator only when the separator doesn't contain
1647             // line-breaking characters.
1648             if (!StringUtils.hasLineBreakCharacter(separatorString)) {
1649                 mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString,
1650                         mConnection.getExpectedSelectionStart(),
1651                         mConnection.getExpectedSelectionEnd());
1652             }
1653             mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString);
1654         } else {
1655             // We have a separator between the word and the cursor: we should show predictions.
1656             inputTransaction.setRequiresUpdateSuggestions();
1657         }
1658     }
1659 
1660     /**
1661      * Factor in auto-caps and manual caps and compute the current caps mode.
1662      * @param settingsValues the current settings values.
1663      * @param keyboardShiftMode the current shift mode of the keyboard. See
1664      *   KeyboardSwitcher#getKeyboardShiftMode() for possible values.
1665      * @return the actual caps mode the keyboard is in right now.
1666      */
getActualCapsMode(final SettingsValues settingsValues, final int keyboardShiftMode)1667     private int getActualCapsMode(final SettingsValues settingsValues,
1668             final int keyboardShiftMode) {
1669         if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) {
1670             return keyboardShiftMode;
1671         }
1672         final int auto = getCurrentAutoCapsState(settingsValues);
1673         if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
1674             return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
1675         }
1676         if (0 != auto) {
1677             return WordComposer.CAPS_MODE_AUTO_SHIFTED;
1678         }
1679         return WordComposer.CAPS_MODE_OFF;
1680     }
1681 
1682     /**
1683      * Gets the current auto-caps state, factoring in the space state.
1684      *
1685      * This method tries its best to do this in the most efficient possible manner. It avoids
1686      * getting text from the editor if possible at all.
1687      * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
1688      * needs to know auto caps state to display the right layout.
1689      *
1690      * @param settingsValues the relevant settings values
1691      * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
1692      */
getCurrentAutoCapsState(final SettingsValues settingsValues)1693     public int getCurrentAutoCapsState(final SettingsValues settingsValues) {
1694         if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
1695 
1696         final EditorInfo ei = getCurrentInputEditorInfo();
1697         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
1698         final int inputType = ei.inputType;
1699         // Warning: this depends on mSpaceState, which may not be the most current value. If
1700         // mSpaceState gets updated later, whoever called this may need to be told about it.
1701         return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations,
1702                 SpaceState.PHANTOM == mSpaceState);
1703     }
1704 
getCurrentRecapitalizeState()1705     public int getCurrentRecapitalizeState() {
1706         if (!mRecapitalizeStatus.isStarted()
1707                 || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
1708                         mConnection.getExpectedSelectionEnd())) {
1709             // Not recapitalizing at the moment
1710             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
1711         }
1712         return mRecapitalizeStatus.getCurrentMode();
1713     }
1714 
1715     /**
1716      * @return the editor info for the current editor
1717      */
getCurrentInputEditorInfo()1718     private EditorInfo getCurrentInputEditorInfo() {
1719         return mLatinIME.getCurrentInputEditorInfo();
1720     }
1721 
1722     /**
1723      * Get information fo previous words from the nth previous word before the cursor as context
1724      * for the suggestion process.
1725      * @param spacingAndPunctuations the current spacing and punctuations settings.
1726      * @param nthPreviousWord reverse index of the word to get (1-indexed)
1727      * @return the information of previous words
1728      */
1729     // TODO: Make this private
getPrevWordsInfoFromNthPreviousWordForSuggestion( final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord)1730     public PrevWordsInfo getPrevWordsInfoFromNthPreviousWordForSuggestion(
1731             final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) {
1732         if (spacingAndPunctuations.mCurrentLanguageHasSpaces) {
1733             // If we are typing in a language with spaces we can just look up the previous
1734             // word information from textview.
1735             return mConnection.getPrevWordsInfoFromNthPreviousWord(
1736                     spacingAndPunctuations, nthPreviousWord);
1737         } else {
1738             return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ?
1739                     PrevWordsInfo.BEGINNING_OF_SENTENCE :
1740                             new PrevWordsInfo(new PrevWordsInfo.WordInfo(
1741                                     mLastComposedWord.mCommittedWord.toString()));
1742         }
1743     }
1744 
1745     /**
1746      * Tests the passed word for resumability.
1747      *
1748      * We can resume suggestions on words whose first code point is a word code point (with some
1749      * nuances: check the code for details).
1750      *
1751      * @param settings the current values of the settings.
1752      * @param word the word to evaluate.
1753      * @return whether it's fine to resume suggestions on this word.
1754      */
isResumableWord(final SettingsValues settings, final String word)1755     private static boolean isResumableWord(final SettingsValues settings, final String word) {
1756         final int firstCodePoint = word.codePointAt(0);
1757         return settings.isWordCodePoint(firstCodePoint)
1758                 && Constants.CODE_SINGLE_QUOTE != firstCodePoint
1759                 && Constants.CODE_DASH != firstCodePoint;
1760     }
1761 
1762     /**
1763      * @param actionId the action to perform
1764      */
performEditorAction(final int actionId)1765     private void performEditorAction(final int actionId) {
1766         mConnection.performEditorAction(actionId);
1767     }
1768 
1769     /**
1770      * Perform the processing specific to inputting TLDs.
1771      *
1772      * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific
1773      * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type
1774      * of character in onCodeInput, but since this gets inputted as a whole string we need to
1775      * do it here specifically. Then, if the last character before the cursor is a period, then
1776      * we cut the dot at the start of ".com". This is because humans tend to type "www.google."
1777      * and then press the ".com" key and instinctively don't expect to get "www.google..com".
1778      *
1779      * @param text the raw text supplied to onTextInput
1780      * @return the text to actually send to the editor
1781      */
performSpecificTldProcessingOnTextInput(final String text)1782     private String performSpecificTldProcessingOnTextInput(final String text) {
1783         if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD
1784                 || !Character.isLetter(text.charAt(1))) {
1785             // Not a tld: do nothing.
1786             return text;
1787         }
1788         // We have a TLD (or something that looks like this): make sure we don't add
1789         // a space even if currently in phantom mode.
1790         mSpaceState = SpaceState.NONE;
1791         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
1792         // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT.
1793         if (Constants.CODE_PERIOD == codePointBeforeCursor) {
1794             return text.substring(1);
1795         } else {
1796             return text;
1797         }
1798     }
1799 
1800     /**
1801      * Handle a press on the settings key.
1802      */
onSettingsKeyPressed()1803     private void onSettingsKeyPressed() {
1804         mLatinIME.displaySettingsDialog();
1805     }
1806 
1807     /**
1808      * Resets the whole input state to the starting state.
1809      *
1810      * This will clear the composing word, reset the last composed word, clear the suggestion
1811      * strip and tell the input connection about it so that it can refresh its caches.
1812      *
1813      * @param newSelStart the new selection start, in java characters.
1814      * @param newSelEnd the new selection end, in java characters.
1815      * @param clearSuggestionStrip whether this method should clear the suggestion strip.
1816      */
1817     // TODO: how is this different from startInput ?!
resetEntireInputState(final int newSelStart, final int newSelEnd, final boolean clearSuggestionStrip)1818     private void resetEntireInputState(final int newSelStart, final int newSelEnd,
1819             final boolean clearSuggestionStrip) {
1820         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
1821         resetComposingState(true /* alsoResetLastComposedWord */);
1822         if (clearSuggestionStrip) {
1823             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
1824         }
1825         mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
1826                 shouldFinishComposition);
1827     }
1828 
1829     /**
1830      * Resets only the composing state.
1831      *
1832      * Compare #resetEntireInputState, which also clears the suggestion strip and resets the
1833      * input connection caches. This only deals with the composing state.
1834      *
1835      * @param alsoResetLastComposedWord whether to also reset the last composed word.
1836      */
resetComposingState(final boolean alsoResetLastComposedWord)1837     private void resetComposingState(final boolean alsoResetLastComposedWord) {
1838         mWordComposer.reset();
1839         if (alsoResetLastComposedWord) {
1840             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
1841         }
1842     }
1843 
1844     /**
1845      * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word
1846      * and obsolete suggestions.
1847      * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions(
1848      *      String, com.android.inputmethod.latin.SuggestedWords)}.
1849      * @param typedWord The typed word as a string.
1850      * @param previousSuggestedWords The previously suggested words.
1851      * @return Obsolete suggestions with the newly typed word.
1852      */
retrieveOlderSuggestions(final String typedWord, final SuggestedWords previousSuggestedWords)1853     private SuggestedWords retrieveOlderSuggestions(final String typedWord,
1854             final SuggestedWords previousSuggestedWords) {
1855         final SuggestedWords oldSuggestedWords =
1856                 previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY
1857                         : previousSuggestedWords;
1858         final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
1859                 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
1860         return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */,
1861                 false /* typedWordValid */, false /* hasAutoCorrectionCandidate */,
1862                 true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle);
1863     }
1864 
1865     /**
1866      * Gets a chunk of text with or the auto-correction indicator underline span as appropriate.
1867      *
1868      * This method looks at the old state of the auto-correction indicator to put or not put
1869      * the underline span as appropriate. It is important to note that this does not correspond
1870      * exactly to whether this word will be auto-corrected to or not: what's important here is
1871      * to keep the same indication as before.
1872      * When we add a new code point to a composing word, we don't know yet if we are going to
1873      * auto-correct it until the suggestions are computed. But in the mean time, we still need
1874      * to display the character and to extend the previous underline. To avoid any flickering,
1875      * the underline should keep the same color it used to have, even if that's not ultimately
1876      * the correct color for this new word. When the suggestions are finished evaluating, we
1877      * will call this method again to fix the color of the underline.
1878      *
1879      * @param text the text on which to maybe apply the span.
1880      * @return the same text, with the auto-correction underline span if that's appropriate.
1881      */
1882     // TODO: Shouldn't this go in some *Utils class instead?
getTextWithUnderline(final String text)1883     private CharSequence getTextWithUnderline(final String text) {
1884         return mIsAutoCorrectionIndicatorOn
1885                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
1886                 : text;
1887     }
1888 
1889     /**
1890      * Sends a DOWN key event followed by an UP key event to the editor.
1891      *
1892      * If possible at all, avoid using this method. It causes all sorts of race conditions with
1893      * the text view because it goes through a different, asynchronous binder. Also, batch edits
1894      * are ignored for key events. Use the normal software input methods instead.
1895      *
1896      * @param keyCode the key code to send inside the key event.
1897      */
sendDownUpKeyEvent(final int keyCode)1898     private void sendDownUpKeyEvent(final int keyCode) {
1899         final long eventTime = SystemClock.uptimeMillis();
1900         mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
1901                 KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1902                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1903         mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
1904                 KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
1905                 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
1906     }
1907 
1908     /**
1909      * Sends a code point to the editor, using the most appropriate method.
1910      *
1911      * Normally we send code points with commitText, but there are some cases (where backward
1912      * compatibility is a concern for example) where we want to use deprecated methods.
1913      *
1914      * @param settingsValues the current values of the settings.
1915      * @param codePoint the code point to send.
1916      */
1917     // TODO: replace these two parameters with an InputTransaction
sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint)1918     private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) {
1919         // TODO: Remove this special handling of digit letters.
1920         // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
1921         if (codePoint >= '0' && codePoint <= '9') {
1922             sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
1923             return;
1924         }
1925 
1926         // TODO: we should do this also when the editor has TYPE_NULL
1927         if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) {
1928             // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
1929             // a hardware keyboard event on pressing enter or delete. This is bad for many
1930             // reasons (there are race conditions with commits) but some applications are
1931             // relying on this behavior so we continue to support it for older apps.
1932             sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
1933         } else {
1934             mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
1935         }
1936     }
1937 
1938     /**
1939      * Promote a phantom space to an actual space.
1940      *
1941      * This essentially inserts a space, and that's it. It just checks the options and the text
1942      * before the cursor are appropriate before doing it.
1943      *
1944      * @param settingsValues the current values of the settings.
1945      */
promotePhantomSpace(final SettingsValues settingsValues)1946     private void promotePhantomSpace(final SettingsValues settingsValues) {
1947         if (settingsValues.shouldInsertSpacesAutomatically()
1948                 && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
1949                 && !mConnection.textBeforeCursorLooksLikeURL()) {
1950             sendKeyCodePoint(settingsValues, Constants.CODE_SPACE);
1951         }
1952     }
1953 
1954     /**
1955      * Do the final processing after a batch input has ended. This commits the word to the editor.
1956      * @param settingsValues the current values of the settings.
1957      * @param suggestedWords suggestedWords to use.
1958      */
onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher)1959     public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues,
1960             final SuggestedWords suggestedWords,
1961             // TODO: remove this argument
1962             final KeyboardSwitcher keyboardSwitcher) {
1963         final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0);
1964         if (TextUtils.isEmpty(batchInputText)) {
1965             return;
1966         }
1967         mConnection.beginBatchEdit();
1968         if (SpaceState.PHANTOM == mSpaceState) {
1969             promotePhantomSpace(settingsValues);
1970         }
1971         final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate();
1972         // Commit except the last word for phrase gesture if the top suggestion is eligible for auto
1973         // commit.
1974         if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) {
1975             // Find the last space
1976             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
1977             if (0 != indexOfLastSpace) {
1978                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
1979                 final SuggestedWords suggestedWordsForLastWordOfPhraseGesture =
1980                         suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture();
1981                 mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture);
1982             }
1983             final String lastWord = batchInputText.substring(indexOfLastSpace);
1984             mWordComposer.setBatchInputWord(lastWord);
1985             setComposingTextInternal(lastWord, 1);
1986         } else {
1987             mWordComposer.setBatchInputWord(batchInputText);
1988             setComposingTextInternal(batchInputText, 1);
1989         }
1990         mConnection.endBatchEdit();
1991         // Space state must be updated before calling updateShiftState
1992         mSpaceState = SpaceState.PHANTOM;
1993         keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues),
1994                 getCurrentRecapitalizeState());
1995     }
1996 
1997     /**
1998      * Commit the typed string to the editor.
1999      *
2000      * This is typically called when we should commit the currently composing word without applying
2001      * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard
2002      * is configured to not do auto-correction at all (because of the settings or the properties of
2003      * the editor). In this case, `separatorString' is set to the separator that was pressed.
2004      * We also come here in a variety of cases with external user action. For example, when the
2005      * cursor is moved while there is a composition, or when the keyboard is closed, or when the
2006      * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected.
2007      * In this case, `separatorString' is set to NOT_A_SEPARATOR.
2008      *
2009      * @param settingsValues the current values of the settings.
2010      * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
2011      */
2012     // TODO: Make this private
commitTyped(final SettingsValues settingsValues, final String separatorString)2013     public void commitTyped(final SettingsValues settingsValues, final String separatorString) {
2014         if (!mWordComposer.isComposingWord()) return;
2015         final String typedWord = mWordComposer.getTypedWord();
2016         if (typedWord.length() > 0) {
2017             commitChosenWord(settingsValues, typedWord,
2018                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString);
2019         }
2020     }
2021 
2022     /**
2023      * Commit the current auto-correction.
2024      *
2025      * This will commit the best guess of the keyboard regarding what the user meant by typing
2026      * the currently composing word. The IME computes suggestions and assigns a confidence score
2027      * to each of them; when it's confident enough in one suggestion, it replaces the typed string
2028      * by this suggestion at commit time. When it's not confident enough, or when it has no
2029      * suggestions, or when the settings or environment does not allow for auto-correction, then
2030      * this method just commits the typed string.
2031      * Note that if suggestions are currently being computed in the background, this method will
2032      * block until the computation returns. This is necessary for consistency (it would be very
2033      * strange if pressing space would commit a different word depending on how fast you press).
2034      *
2035      * @param settingsValues the current value of the settings.
2036      * @param separator the separator that's causing the commit to happen.
2037      */
commitCurrentAutoCorrection(final SettingsValues settingsValues, final String separator, final LatinIME.UIHandler handler)2038     private void commitCurrentAutoCorrection(final SettingsValues settingsValues,
2039             final String separator,
2040             // TODO: Remove this argument.
2041             final LatinIME.UIHandler handler) {
2042         // Complete any pending suggestions query first
2043         if (handler.hasPendingUpdateSuggestions()) {
2044             handler.cancelUpdateSuggestionStrip();
2045             // To know the input style here, we should retrieve the in-flight "update suggestions"
2046             // message and read its arg1 member here. However, the Handler class does not let
2047             // us retrieve this message, so we can't do that. But in fact, we notice that
2048             // we only ever come here when the input style was typing. In the case of batch
2049             // input, we update the suggestions synchronously when the tail batch comes. Likewise
2050             // for application-specified completions. As for recorrections, we never auto-correct,
2051             // so we don't come here either. Hence, the input style is necessarily
2052             // INPUT_STYLE_TYPING.
2053             performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING);
2054         }
2055         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
2056         final String typedWord = mWordComposer.getTypedWord();
2057         final String autoCorrection = (typedAutoCorrection != null)
2058                 ? typedAutoCorrection : typedWord;
2059         if (autoCorrection != null) {
2060             if (TextUtils.isEmpty(typedWord)) {
2061                 throw new RuntimeException("We have an auto-correction but the typed word "
2062                         + "is empty? Impossible! I must commit suicide.");
2063             }
2064             commitChosenWord(settingsValues, autoCorrection,
2065                     LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator);
2066             if (!typedWord.equals(autoCorrection)) {
2067                 // This will make the correction flash for a short while as a visual clue
2068                 // to the user that auto-correction happened. It has no other effect; in particular
2069                 // note that this won't affect the text inside the text field AT ALL: it only makes
2070                 // the segment of text starting at the supplied index and running for the length
2071                 // of the auto-correction flash. At this moment, the "typedWord" argument is
2072                 // ignored by TextView.
2073                 mConnection.commitCorrection(new CorrectionInfo(
2074                         mConnection.getExpectedSelectionEnd() - autoCorrection.length(),
2075                         typedWord, autoCorrection));
2076             }
2077         }
2078     }
2079 
2080     /**
2081      * Commits the chosen word to the text field and saves it for later retrieval.
2082      *
2083      * @param settingsValues the current values of the settings.
2084      * @param chosenWord the word we want to commit.
2085      * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
2086      * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
2087      */
commitChosenWord(final SettingsValues settingsValues, final String chosenWord, final int commitType, final String separatorString)2088     private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
2089             final int commitType, final String separatorString) {
2090         final SuggestedWords suggestedWords = mSuggestedWords;
2091         final CharSequence chosenWordWithSuggestions =
2092                 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
2093                         suggestedWords);
2094         // When we are composing word, get previous words information from the 2nd previous word
2095         // because the 1st previous word is the word to be committed. Otherwise get previous words
2096         // information from the 1st previous word.
2097         final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord(
2098                 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
2099         mConnection.commitText(chosenWordWithSuggestions, 1);
2100         // Add the word to the user history dictionary
2101         performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo);
2102         // TODO: figure out here if this is an auto-correct or if the best word is actually
2103         // what user typed. Note: currently this is done much later in
2104         // LastComposedWord#didCommitTypedWord by string equality of the remembered
2105         // strings.
2106         mLastComposedWord = mWordComposer.commitWord(commitType,
2107                 chosenWordWithSuggestions, separatorString, prevWordsInfo);
2108     }
2109 
2110     /**
2111      * Retry resetting caches in the rich input connection.
2112      *
2113      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
2114      * This method handles the retry, and re-schedules a new retry if we still can't access.
2115      * We only retry up to 5 times before giving up.
2116      *
2117      * @param tryResumeSuggestions Whether we should resume suggestions or not.
2118      * @param remainingTries How many times we may try again before giving up.
2119      * @return whether true if the caches were successfully reset, false otherwise.
2120      */
2121     // TODO: make this private
retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, final int remainingTries, final LatinIME.UIHandler handler)2122     public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions,
2123             final int remainingTries,
2124             // TODO: remove these arguments
2125             final LatinIME.UIHandler handler) {
2126         final boolean shouldFinishComposition = mConnection.hasSelection()
2127                 || !mConnection.isCursorPositionKnown();
2128         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
2129                 mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(),
2130                 shouldFinishComposition)) {
2131             if (0 < remainingTries) {
2132                 handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
2133                 return false;
2134             }
2135             // If remainingTries is 0, we should stop waiting for new tries, however we'll still
2136             // return true as we need to perform other tasks (for example, loading the keyboard).
2137         }
2138         mConnection.tryFixLyingCursorPosition();
2139         if (tryResumeSuggestions) {
2140             // This is triggered when starting input anew, so we want to include the resumed
2141             // word in suggestions.
2142             handler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
2143                     true /* shouldDelay */);
2144         }
2145         return true;
2146     }
2147 
getSuggestedWords(final SettingsValues settingsValues, final ProximityInfo proximityInfo, final int keyboardShiftMode, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback)2148     public void getSuggestedWords(final SettingsValues settingsValues,
2149             final ProximityInfo proximityInfo, final int keyboardShiftMode, final int inputStyle,
2150             final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
2151         mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions(
2152                 getActualCapsMode(settingsValues, keyboardShiftMode));
2153         mSuggest.getSuggestedWords(mWordComposer,
2154                 getPrevWordsInfoFromNthPreviousWordForSuggestion(
2155                         settingsValues.mSpacingAndPunctuations,
2156                         // Get the word on which we should search the bigrams. If we are composing
2157                         // a word, it's whatever is *before* the half-committed word in the buffer,
2158                         // hence 2; if we aren't, we should just skip whitespace if any, so 1.
2159                         mWordComposer.isComposingWord() ? 2 : 1),
2160                 proximityInfo,
2161                 new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive,
2162                         settingsValues.mPhraseGestureEnabled,
2163                         settingsValues.mAdditionalFeaturesSettingValues),
2164                 settingsValues.mAutoCorrectionEnabledPerUserSettings,
2165                 inputStyle, sequenceNumber, callback);
2166     }
2167 
2168     /**
2169      * Used as an injection point for each call of
2170      * {@link RichInputConnection#setComposingText(CharSequence, int)}.
2171      *
2172      * <p>Currently using this method is optional and you can still directly call
2173      * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to
2174      * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p>
2175      * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p>
2176      *
2177      * @param newComposingText the composing text to be set
2178      * @param newCursorPosition the new cursor position
2179      */
setComposingTextInternal(final CharSequence newComposingText, final int newCursorPosition)2180     private void setComposingTextInternal(final CharSequence newComposingText,
2181             final int newCursorPosition) {
2182         setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition,
2183                 Color.TRANSPARENT, newComposingText.length());
2184     }
2185 
2186     /**
2187      * Equivalent to {@link #setComposingTextInternal(CharSequence, int)} except that this method
2188      * allows to set {@link BackgroundColorSpan} to the composing text with the given color.
2189      *
2190      * <p>TODO: Currently the background color is exclusive with the black underline, which is
2191      * automatically added by the framework. We need to change the framework if we need to have both
2192      * of them at the same time.</p>
2193      * <p>TODO: Should we move this method to {@link RichInputConnection}?</p>
2194      *
2195      * @param newComposingText the composing text to be set
2196      * @param newCursorPosition the new cursor position
2197      * @param backgroundColor the background color to be set to the composing text. Set
2198      * {@link Color#TRANSPARENT} to disable the background color.
2199      * @param coloredTextLength the length of text, in Java chars, which should be rendered with
2200      * the given background color.
2201      */
setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, final int newCursorPosition, final int backgroundColor, final int coloredTextLength)2202     private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText,
2203             final int newCursorPosition, final int backgroundColor, final int coloredTextLength) {
2204         final CharSequence composingTextToBeSet;
2205         if (backgroundColor == Color.TRANSPARENT) {
2206             composingTextToBeSet = newComposingText;
2207         } else {
2208             final SpannableString spannable = new SpannableString(newComposingText);
2209             final BackgroundColorSpan backgroundColorSpan =
2210                     new BackgroundColorSpan(backgroundColor);
2211             final int spanLength = Math.min(coloredTextLength, spannable.length());
2212             spannable.setSpan(backgroundColorSpan, 0, spanLength,
2213                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
2214             composingTextToBeSet = spannable;
2215         }
2216         mConnection.setComposingText(composingTextToBeSet, newCursorPosition);
2217     }
2218 
2219     //////////////////////////////////////////////////////////////////////////////////////////////
2220     // Following methods are tentatively placed in this class for the integration with
2221     // TextDecorator.
2222     // TODO: Decouple things that are not related to the input logic.
2223     //////////////////////////////////////////////////////////////////////////////////////////////
2224 
2225     /**
2226      * Sets the UI operator for {@link TextDecorator}.
2227      * @param uiOperator the UI operator which should be associated with {@link TextDecorator}.
2228      */
setTextDecoratorUi(final TextDecoratorUiOperator uiOperator)2229     public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) {
2230         mTextDecorator.setUiOperator(uiOperator);
2231     }
2232 
2233     /**
2234      * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is
2235      * called.
2236      * @param info The wrapper object with which we can access cursor/anchor info.
2237      */
onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info)2238     public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) {
2239         mTextDecorator.onUpdateCursorAnchorInfo(info);
2240     }
2241 
2242     /**
2243      * Must be called when {@link InputMethodService#updateFullscreenMode} is called.
2244      * @param isFullscreen {@code true} if the input method is in full-screen mode.
2245      */
onUpdateFullscreenMode(final boolean isFullscreen)2246     public void onUpdateFullscreenMode(final boolean isFullscreen) {
2247         mTextDecorator.notifyFullScreenMode(isFullscreen);
2248     }
2249 
2250     /**
2251      * Must be called from {@link LatinIME#addWordToUserDictionary(String)}.
2252      */
onAddWordToUserDictionary()2253     public void onAddWordToUserDictionary() {
2254         mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
2255         mTextDecorator.reset();
2256     }
2257 
2258     /**
2259      * Returns whether the add to dictionary indicator should be shown or not.
2260      * @param lastComposedWord the last composed word information.
2261      * @param settingsValues the current settings value.
2262      * @return {@code true} if the commit indicator should be shown.
2263      */
shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord, final SettingsValues settingsValues)2264     private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord,
2265             final SettingsValues settingsValues) {
2266         if (!mConnection.isCursorAnchorInfoMonitorEnabled()) {
2267             // We cannot help in this case because we are heavily relying on this new API.
2268             return false;
2269         }
2270         if (!settingsValues.mShouldShowUiToAcceptTypedWord) {
2271             return false;
2272         }
2273         if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) {
2274             return false;
2275         }
2276         if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) {
2277             return false;
2278         }
2279         if (!mDictionaryFacilitator.isUserDictionaryEnabled()) {
2280             return false;
2281         }
2282         return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord,
2283                 true /* ignoreCase */);
2284     }
2285 }
2286