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