InputLogic.java revision 69a57bcdcd254b8e2dfbc367ef130114634c51a5
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.latin.inputlogic;
18
19import android.os.SystemClock;
20import android.text.TextUtils;
21import android.util.Log;
22import android.view.KeyCharacterMap;
23import android.view.KeyEvent;
24import android.view.inputmethod.CorrectionInfo;
25import android.view.inputmethod.EditorInfo;
26
27import com.android.inputmethod.compat.SuggestionSpanUtils;
28import com.android.inputmethod.event.EventInterpreter;
29import com.android.inputmethod.keyboard.Keyboard;
30import com.android.inputmethod.keyboard.KeyboardSwitcher;
31import com.android.inputmethod.keyboard.MainKeyboardView;
32import com.android.inputmethod.latin.Constants;
33import com.android.inputmethod.latin.LastComposedWord;
34import com.android.inputmethod.latin.LatinIME;
35import com.android.inputmethod.latin.LatinImeLogger;
36import com.android.inputmethod.latin.RichInputConnection;
37import com.android.inputmethod.latin.SubtypeSwitcher;
38import com.android.inputmethod.latin.Suggest;
39import com.android.inputmethod.latin.SuggestedWords;
40import com.android.inputmethod.latin.WordComposer;
41import com.android.inputmethod.latin.define.ProductionFlag;
42import com.android.inputmethod.latin.settings.Settings;
43import com.android.inputmethod.latin.settings.SettingsValues;
44import com.android.inputmethod.latin.utils.CollectionUtils;
45import com.android.inputmethod.latin.utils.InputTypeUtils;
46import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
47import com.android.inputmethod.latin.utils.RecapitalizeStatus;
48import com.android.inputmethod.latin.utils.StringUtils;
49import com.android.inputmethod.research.ResearchLogger;
50
51import java.util.TreeSet;
52
53/**
54 * This class manages the input logic.
55 */
56public final class InputLogic {
57    private static final String TAG = InputLogic.class.getSimpleName();
58
59    // TODO : Remove this member when we can.
60    private final LatinIME mLatinIME;
61
62    // TODO : make all these fields private as soon as possible.
63    // Current space state of the input method. This can be any of the above constants.
64    public int mSpaceState;
65    // Never null
66    public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
67    public Suggest mSuggest;
68    // The event interpreter should never be null.
69    public EventInterpreter mEventInterpreter;
70
71    public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
72    public final WordComposer mWordComposer;
73    public final RichInputConnection mConnection;
74    public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
75
76    // Keep track of the last selection range to decide if we need to show word alternatives
77    public static final int NOT_A_CURSOR_POSITION = -1;
78    public int mLastSelectionStart = NOT_A_CURSOR_POSITION;
79    public int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
80
81    public int mDeleteCount;
82    public long mLastKeyTime;
83    public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
84
85    // Keeps track of most recently inserted text (multi-character key) for reverting
86    public String mEnteredText;
87
88    // TODO: This boolean is persistent state and causes large side effects at unexpected times.
89    // Find a way to remove it for readability.
90    public boolean mIsAutoCorrectionIndicatorOn;
91
92    public InputLogic(final LatinIME latinIME) {
93        mLatinIME = latinIME;
94        mWordComposer = new WordComposer();
95        mEventInterpreter = new EventInterpreter(latinIME);
96        mConnection = new RichInputConnection(latinIME);
97    }
98
99    public void startInput(final boolean restarting) {
100    }
101
102    public void finishInput() {
103    }
104
105    public void onCodeInput(final int codePoint, final int x, final int y,
106            // TODO: remove these three arguments
107            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher,
108            final SubtypeSwitcher subtypeSwitcher) {
109        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
110            ResearchLogger.latinIME_onCodeInput(codePoint, x, y);
111        }
112        final SettingsValues settingsValues = Settings.getInstance().getCurrent();
113        final long when = SystemClock.uptimeMillis();
114        if (codePoint != Constants.CODE_DELETE
115                || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
116            mDeleteCount = 0;
117        }
118        mLastKeyTime = when;
119        mConnection.beginBatchEdit();
120        final KeyboardSwitcher switcher = keyboardSwitcher;
121        // The space state depends only on the last character pressed and its own previous
122        // state. Here, we revert the space state to neutral if the key is actually modifying
123        // the input contents (any non-shift key), which is what we should do for
124        // all inputs that do not result in a special state. Each character handling is then
125        // free to override the state as they see fit.
126        final int spaceState = mSpaceState;
127        if (!mWordComposer.isComposingWord()) {
128            mIsAutoCorrectionIndicatorOn = false;
129        }
130
131        // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
132        if (codePoint != Constants.CODE_SPACE) {
133            handler.cancelDoubleSpacePeriodTimer();
134        }
135
136        boolean didAutoCorrect = false;
137        switch (codePoint) {
138        case Constants.CODE_DELETE:
139            mSpaceState = SpaceState.NONE;
140            handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher);
141            LatinImeLogger.logOnDelete(x, y);
142            break;
143        case Constants.CODE_SHIFT:
144            // Note: Calling back to the keyboard on Shift key is handled in
145            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
146            final Keyboard currentKeyboard = switcher.getKeyboard();
147            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
148                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
149                // alphabetic shift and shift while in symbol layout.
150                performRecapitalization(settingsValues, keyboardSwitcher);
151            }
152            break;
153        case Constants.CODE_CAPSLOCK:
154            // Note: Changing keyboard to shift lock state is handled in
155            // {@link KeyboardSwitcher#onCodeInput(int)}.
156            break;
157        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
158            // Note: Calling back to the keyboard on symbol key is handled in
159            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
160            break;
161        case Constants.CODE_SETTINGS:
162            onSettingsKeyPressed();
163            break;
164        case Constants.CODE_SHORTCUT:
165            subtypeSwitcher.switchToShortcutIME(mLatinIME);
166            break;
167        case Constants.CODE_ACTION_NEXT:
168            performEditorAction(EditorInfo.IME_ACTION_NEXT);
169            break;
170        case Constants.CODE_ACTION_PREVIOUS:
171            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
172            break;
173        case Constants.CODE_LANGUAGE_SWITCH:
174            handleLanguageSwitchKey();
175            break;
176        case Constants.CODE_EMOJI:
177            // Note: Switching emoji keyboard is being handled in
178            // {@link KeyboardState#onCodeInput(int,int)}.
179            break;
180        case Constants.CODE_ENTER:
181            final EditorInfo editorInfo = getCurrentInputEditorInfo();
182            final int imeOptionsActionId =
183                    InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
184            if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
185                // Either we have an actionLabel and we should performEditorAction with actionId
186                // regardless of its value.
187                performEditorAction(editorInfo.actionId);
188            } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
189                // We didn't have an actionLabel, but we had another action to execute.
190                // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
191                // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
192                // means there should be an action and the app didn't bother to set a specific
193                // code for it - presumably it only handles one. It does not have to be treated
194                // in any specific way: anything that is not IME_ACTION_NONE should be sent to
195                // performEditorAction.
196                performEditorAction(imeOptionsActionId);
197            } else {
198                // No action label, and the action from imeOptions is NONE: this is a regular
199                // enter key that should input a carriage return.
200                didAutoCorrect = handleNonSpecialCharacter(settingsValues,
201                        Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
202            }
203            break;
204        case Constants.CODE_SHIFT_ENTER:
205            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
206                    Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
207            break;
208        default:
209            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
210                    codePoint, x, y, spaceState, keyboardSwitcher, handler);
211            break;
212        }
213        switcher.onCodeInput(codePoint);
214        // Reset after any single keystroke, except shift, capslock, and symbol-shift
215        if (!didAutoCorrect && codePoint != Constants.CODE_SHIFT
216                && codePoint != Constants.CODE_CAPSLOCK
217                && codePoint != Constants.CODE_SWITCH_ALPHA_SYMBOL)
218            mLastComposedWord.deactivate();
219        if (Constants.CODE_DELETE != codePoint) {
220            mEnteredText = null;
221        }
222        mConnection.endBatchEdit();
223    }
224
225    /**
226     * Handle inputting a code point to the editor.
227     *
228     * Non-special keys are those that generate a single code point.
229     * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
230     * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
231     * any key that results in multiple code points like the ".com" key.
232     *
233     * @param settingsValues The current settings values.
234     * @param codePoint the code point associated with the key.
235     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
236     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
237     * @param spaceState the space state at start of the batch input.
238     * @return whether this caused an auto-correction to happen.
239     */
240    private boolean handleNonSpecialCharacter(final SettingsValues settingsValues,
241            final int codePoint, final int x, final int y, final int spaceState,
242            // TODO: remove these arguments
243            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
244        mSpaceState = SpaceState.NONE;
245        final boolean didAutoCorrect;
246        if (settingsValues.isWordSeparator(codePoint)
247                || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
248            didAutoCorrect = handleSeparator(settingsValues, codePoint, x, y, spaceState,
249                    keyboardSwitcher, handler);
250        } else {
251            didAutoCorrect = false;
252            if (SpaceState.PHANTOM == spaceState) {
253                if (settingsValues.mIsInternal) {
254                    if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
255                        LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
256                                mWordComposer);
257                    }
258                }
259                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
260                    // If we are in the middle of a recorrection, we need to commit the recorrection
261                    // first so that we can insert the character at the current cursor position.
262                    resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
263                } else {
264                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
265                }
266            }
267            final int keyX, keyY;
268            final Keyboard keyboard = keyboardSwitcher.getKeyboard();
269            if (keyboard != null && keyboard.hasProximityCharsCorrection(codePoint)) {
270                keyX = x;
271                keyY = y;
272            } else {
273                keyX = Constants.NOT_A_COORDINATE;
274                keyY = Constants.NOT_A_COORDINATE;
275            }
276            handleNonSeparator(settingsValues, codePoint, keyX, keyY, spaceState,
277                    keyboardSwitcher, handler);
278        }
279        return didAutoCorrect;
280    }
281
282    /**
283     * Handle a non-separator.
284     * @param settingsValues The current settings values.
285     * @param codePoint the code point associated with the key.
286     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
287     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
288     * @param spaceState the space state at start of the batch input.
289     */
290    private void handleNonSeparator(final SettingsValues settingsValues,
291            final int codePoint, final int x, final int y, final int spaceState,
292            // TODO: Remove these arguments
293            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
294        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
295        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
296        // which has the same name as other handle* methods but is not the same.
297        boolean isComposingWord = mWordComposer.isComposingWord();
298
299        // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
300        // See onStartBatchInput() to see how to do it.
301        if (SpaceState.PHANTOM == spaceState && !settingsValues.isWordConnector(codePoint)) {
302            if (isComposingWord) {
303                // Sanity check
304                throw new RuntimeException("Should not be composing here");
305            }
306            promotePhantomSpace(settingsValues);
307        }
308
309        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
310            // If we are in the middle of a recorrection, we need to commit the recorrection
311            // first so that we can insert the character at the current cursor position.
312            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
313            isComposingWord = false;
314        }
315        // We want to find out whether to start composing a new word with this character. If so,
316        // we need to reset the composing state and switch isComposingWord. The order of the
317        // tests is important for good performance.
318        // We only start composing if we're not already composing.
319        if (!isComposingWord
320        // We only start composing if this is a word code point. Essentially that means it's a
321        // a letter or a word connector.
322                && settingsValues.isWordCodePoint(codePoint)
323        // We never go into composing state if suggestions are not requested.
324                && settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation) &&
325        // In languages with spaces, we only start composing a word when we are not already
326        // touching a word. In languages without spaces, the above conditions are sufficient.
327                (!mConnection.isCursorTouchingWord(settingsValues)
328                        || !settingsValues.mCurrentLanguageHasSpaces)) {
329            // Reset entirely the composing state anyway, then start composing a new word unless
330            // the character is a single quote or a dash. The idea here is, single quote and dash
331            // are not separators and they should be treated as normal characters, except in the
332            // first position where they should not start composing a word.
333            isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint
334                    && Constants.CODE_DASH != codePoint);
335            // Here we don't need to reset the last composed word. It will be reset
336            // when we commit this one, if we ever do; if on the other hand we backspace
337            // it entirely and resume suggestions on the previous word, we'd like to still
338            // have touch coordinates for it.
339            resetComposingState(false /* alsoResetLastComposedWord */);
340        }
341        if (isComposingWord) {
342            final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView();
343            // TODO: We should reconsider which coordinate system should be used to represent
344            // keyboard event.
345            final int keyX = mainKeyboardView.getKeyX(x);
346            final int keyY = mainKeyboardView.getKeyY(y);
347            mWordComposer.add(codePoint, keyX, keyY);
348            // If it's the first letter, make note of auto-caps state
349            if (mWordComposer.size() == 1) {
350                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
351                // yet, so the word we want is the 1st word before the cursor.
352                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
353                        getActualCapsMode(keyboardSwitcher),
354                        getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
355            }
356            mConnection.setComposingText(getTextWithUnderline(
357                    mWordComposer.getTypedWord()), 1);
358        } else {
359            final boolean swapWeakSpace = maybeStripSpace(settingsValues,
360                    codePoint, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
361
362            sendKeyCodePoint(codePoint);
363
364            if (swapWeakSpace) {
365                swapSwapperAndSpace(keyboardSwitcher);
366                mSpaceState = SpaceState.WEAK;
367            }
368            // In case the "add to dictionary" hint was still displayed.
369            mLatinIME.dismissAddToDictionaryHint();
370        }
371        handler.postUpdateSuggestionStrip();
372        if (settingsValues.mIsInternal) {
373            LatinImeLoggerUtils.onNonSeparator((char)codePoint, x, y);
374        }
375    }
376
377    /**
378     * Handle input of a separator code point.
379     * @param settingsValues The current settings values.
380     * @param codePoint the code point associated with the key.
381     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
382     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
383     * @param spaceState the space state at start of the batch input.
384     * @return whether this caused an auto-correction to happen.
385     */
386    private boolean handleSeparator(final SettingsValues settingsValues,
387            final int codePoint, final int x, final int y, final int spaceState,
388            // TODO: remove these arguments
389            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
390        boolean didAutoCorrect = false;
391        // We avoid sending spaces in languages without spaces if we were composing.
392        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
393                && !settingsValues.mCurrentLanguageHasSpaces
394                && mWordComposer.isComposingWord();
395        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
396            // If we are in the middle of a recorrection, we need to commit the recorrection
397            // first so that we can insert the separator at the current cursor position.
398            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
399        }
400        // isComposingWord() may have changed since we stored wasComposing
401        if (mWordComposer.isComposingWord()) {
402            if (settingsValues.mCorrectionEnabled) {
403                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
404                        : StringUtils.newSingleCodePointString(codePoint);
405                commitCurrentAutoCorrection(settingsValues, separator, handler);
406                didAutoCorrect = true;
407            } else {
408                commitTyped(StringUtils.newSingleCodePointString(codePoint));
409            }
410        }
411
412        final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
413                Constants.SUGGESTION_STRIP_COORDINATE == x);
414
415        if (SpaceState.PHANTOM == spaceState &&
416                settingsValues.isUsuallyPrecededBySpace(codePoint)) {
417            promotePhantomSpace(settingsValues);
418        }
419        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
420            ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord());
421        }
422
423        if (!shouldAvoidSendingCode) {
424            sendKeyCodePoint(codePoint);
425        }
426
427        if (Constants.CODE_SPACE == codePoint) {
428            if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation)) {
429                if (maybeDoubleSpacePeriod(settingsValues, keyboardSwitcher, handler)) {
430                    mSpaceState = SpaceState.DOUBLE;
431                } else if (!mLatinIME.isShowingPunctuationList()) {
432                    mSpaceState = SpaceState.WEAK;
433                }
434            }
435
436            handler.startDoubleSpacePeriodTimer();
437            handler.postUpdateSuggestionStrip();
438        } else {
439            if (swapWeakSpace) {
440                swapSwapperAndSpace(keyboardSwitcher);
441                mSpaceState = SpaceState.SWAP_PUNCTUATION;
442            } else if (SpaceState.PHANTOM == spaceState
443                    && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
444                // If we are in phantom space state, and the user presses a separator, we want to
445                // stay in phantom space state so that the next keypress has a chance to add the
446                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
447                // then insert a comma and go on to typing the next word, I want the space to be
448                // inserted automatically before the next word, the same way it is when I don't
449                // input the comma.
450                // The case is a little different if the separator is a space stripper. Such a
451                // separator does not normally need a space on the right (that's the difference
452                // between swappers and strippers), so we should not stay in phantom space state if
453                // the separator is a stripper. Hence the additional test above.
454                mSpaceState = SpaceState.PHANTOM;
455            }
456
457            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
458            // already displayed or not, so it's okay.
459            mLatinIME.setPunctuationSuggestions();
460        }
461        if (settingsValues.mIsInternal) {
462            LatinImeLoggerUtils.onSeparator((char)codePoint, x, y);
463        }
464
465        keyboardSwitcher.updateShiftState();
466        return didAutoCorrect;
467    }
468
469    /**
470     * Handle a press on the backspace key.
471     * @param settingsValues The current settings values.
472     * @param spaceState The space state at start of this batch edit.
473     */
474    private void handleBackspace(final SettingsValues settingsValues, final int spaceState,
475            // TODO: remove these arguments
476            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
477        mDeleteCount++;
478
479        // In many cases, we may have to put the keyboard in auto-shift state again. However
480        // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
481        // during key repeat.
482        handler.postUpdateShiftState();
483
484        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
485            // If we are in the middle of a recorrection, we need to commit the recorrection
486            // first so that we can remove the character at the current cursor position.
487            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
488            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
489        }
490        if (mWordComposer.isComposingWord()) {
491            if (mWordComposer.isBatchMode()) {
492                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
493                    final String word = mWordComposer.getTypedWord();
494                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
495                }
496                final String rejectedSuggestion = mWordComposer.getTypedWord();
497                mWordComposer.reset();
498                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
499            } else {
500                mWordComposer.deleteLast();
501            }
502            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
503            handler.postUpdateSuggestionStrip();
504            if (!mWordComposer.isComposingWord()) {
505                // If we just removed the last character, auto-caps mode may have changed so we
506                // need to re-evaluate.
507                keyboardSwitcher.updateShiftState();
508            }
509        } else {
510            if (mLastComposedWord.canRevertCommit()) {
511                if (settingsValues.mIsInternal) {
512                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
513                }
514                mLatinIME.revertCommit();
515                return;
516            }
517            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
518                // Cancel multi-character input: remove the text we just entered.
519                // This is triggered on backspace after a key that inputs multiple characters,
520                // like the smiley key or the .com key.
521                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
522                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
523                    ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
524                }
525                mEnteredText = null;
526                // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
527                // In addition we know that spaceState is false, and that we should not be
528                // reverting any autocorrect at this point. So we can safely return.
529                return;
530            }
531            if (SpaceState.DOUBLE == spaceState) {
532                handler.cancelDoubleSpacePeriodTimer();
533                if (mConnection.revertDoubleSpacePeriod()) {
534                    // No need to reset mSpaceState, it has already be done (that's why we
535                    // receive it as a parameter)
536                    return;
537                }
538            } else if (SpaceState.SWAP_PUNCTUATION == spaceState) {
539                if (mConnection.revertSwapPunctuation()) {
540                    // Likewise
541                    return;
542                }
543            }
544
545            // No cancelling of commit/double space/swap: we have a regular backspace.
546            // We should backspace one char and restart suggestion if at the end of a word.
547            if (mLastSelectionStart != mLastSelectionEnd) {
548                // If there is a selection, remove it.
549                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
550                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
551                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
552                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
553                // but we want to set it right away to avoid it being used with the wrong values
554                // later (typically, in a subsequent press on backspace).
555                mLastSelectionEnd = mLastSelectionStart;
556                mConnection.deleteSurroundingText(numCharsDeleted, 0);
557                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
558                    ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
559                            false /* shouldUncommitLogUnit */);
560                }
561            } else {
562                // There is no selection, just delete one character.
563                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
564                    // This should never happen.
565                    Log.e(TAG, "Backspace when we don't know the selection position");
566                }
567                if (mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean() ||
568                        settingsValues.mInputAttributes.isTypeNull()) {
569                    // There are two possible reasons to send a key event: either the field has
570                    // type TYPE_NULL, in which case the keyboard should send events, or we are
571                    // running in backward compatibility mode. Before Jelly bean, the keyboard
572                    // would simulate a hardware keyboard event on pressing enter or delete. This
573                    // is bad for many reasons (there are race conditions with commits) but some
574                    // applications are relying on this behavior so we continue to support it for
575                    // older apps, so we retain this behavior if the app has target SDK < JellyBean.
576                    sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
577                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
578                        sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
579                    }
580                } else {
581                    final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
582                    if (codePointBeforeCursor == Constants.NOT_A_CODE) {
583                        // Nothing to delete before the cursor.
584                        return;
585                    }
586                    final int lengthToDelete =
587                            Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
588                    mConnection.deleteSurroundingText(lengthToDelete, 0);
589                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
590                        ResearchLogger.latinIME_handleBackspace(lengthToDelete,
591                                true /* shouldUncommitLogUnit */);
592                    }
593                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
594                        final int codePointBeforeCursorToDeleteAgain =
595                                mConnection.getCodePointBeforeCursor();
596                        if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
597                            final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
598                                    codePointBeforeCursorToDeleteAgain) ? 2 : 1;
599                            mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
600                            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
601                                ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
602                                        true /* shouldUncommitLogUnit */);
603                            }
604                        }
605                    }
606                }
607            }
608            // TODO: move mDisplayOrientation to CurrentSettings.
609            if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation)
610                    && settingsValues.mCurrentLanguageHasSpaces) {
611                mLatinIME.restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
612            }
613            // We just removed a character. We need to update the auto-caps state.
614            keyboardSwitcher.updateShiftState();
615        }
616    }
617
618    /**
619     * Handle a press on the language switch key (the "globe key")
620     */
621    private void handleLanguageSwitchKey() {
622        mLatinIME.handleLanguageSwitchKey();
623    }
624
625    // TODO: Make this private
626    // TODO: Remove this argument
627    public void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) {
628        final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
629        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
630        if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
631            mConnection.deleteSurroundingText(2, 0);
632            final String text = lastTwo.charAt(1) + " ";
633            mConnection.commitText(text, 1);
634            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
635                ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
636            }
637            keyboardSwitcher.updateShiftState();
638        }
639    }
640
641    /*
642     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
643     * @param settingsValues The current settings values.
644     * @param codePoint The code point that is about to be inserted.
645     * @param spaceState The space state at start of this batch edit.
646     * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip.
647     * @return whether we should swap the space instead of removing it.
648     */
649    // TODO: Make this private
650    public boolean maybeStripSpace(final SettingsValues settingsValues,
651            final int code, final int spaceState, final boolean isFromSuggestionStrip) {
652        if (Constants.CODE_ENTER == code && SpaceState.SWAP_PUNCTUATION == spaceState) {
653            mConnection.removeTrailingSpace();
654            return false;
655        }
656        if ((SpaceState.WEAK == spaceState || SpaceState.SWAP_PUNCTUATION == spaceState)
657                && isFromSuggestionStrip) {
658            if (settingsValues.isUsuallyPrecededBySpace(code)) return false;
659            if (settingsValues.isUsuallyFollowedBySpace(code)) return true;
660            mConnection.removeTrailingSpace();
661        }
662        return false;
663    }
664
665    private boolean maybeDoubleSpacePeriod(final SettingsValues settingsValues,
666            // TODO: remove these arguments
667            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
668        if (!settingsValues.mUseDoubleSpacePeriod) return false;
669        if (!handler.isAcceptingDoubleSpacePeriod()) return false;
670        // We only do this when we see two spaces and an accepted code point before the cursor.
671        // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
672        final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
673        if (null == lastThree) return false;
674        final int length = lastThree.length();
675        if (length < 3) return false;
676        if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
677        if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
678        // We know there are spaces in pos -1 and -2, and we have at least three chars.
679        // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
680        // so this is fine.
681        final int firstCodePoint =
682                Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
683                        Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
684        if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
685            handler.cancelDoubleSpacePeriodTimer();
686            mConnection.deleteSurroundingText(2, 0);
687            final String textToInsert = new String(
688                    new int[] { settingsValues.mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
689            mConnection.commitText(textToInsert, 1);
690            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
691                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
692                        false /* isBatchMode */);
693            }
694            mWordComposer.discardPreviousWordForSuggestion();
695            keyboardSwitcher.updateShiftState();
696            return true;
697        }
698        return false;
699    }
700
701    private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
702        // TODO: Check again whether there really ain't a better way to check this.
703        // TODO: This should probably be language-dependant...
704        return Character.isLetterOrDigit(codePoint)
705                || codePoint == Constants.CODE_SINGLE_QUOTE
706                || codePoint == Constants.CODE_DOUBLE_QUOTE
707                || codePoint == Constants.CODE_CLOSING_PARENTHESIS
708                || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
709                || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
710                || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
711                || codePoint == Constants.CODE_PLUS
712                || codePoint == Constants.CODE_PERCENT
713                || Character.getType(codePoint) == Character.OTHER_SYMBOL;
714    }
715
716    /**
717     * Performs a recapitalization event.
718     * @param settingsValues The current settings values.
719     */
720    public void performRecapitalization(final SettingsValues settingsValues,
721            // TODO: remove this argument.
722            final KeyboardSwitcher keyboardSwitcher) {
723        if (mLastSelectionStart == mLastSelectionEnd) {
724            return; // No selection
725        }
726        // If we have a recapitalize in progress, use it; otherwise, create a new one.
727        if (!mRecapitalizeStatus.isActive()
728                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
729            final CharSequence selectedText =
730                    mConnection.getSelectedText(0 /* flags, 0 for no styles */);
731            if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
732            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
733                    selectedText.toString(),
734                    settingsValues.mLocale, settingsValues.mWordSeparators);
735            // We trim leading and trailing whitespace.
736            mRecapitalizeStatus.trim();
737            // Trimming the object may have changed the length of the string, and we need to
738            // reposition the selection handles accordingly. As this result in an IPC call,
739            // only do it if it's actually necessary, in other words if the recapitalize status
740            // is not set at the same place as before.
741            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
742                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
743                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
744            }
745        }
746        mConnection.finishComposingText();
747        mRecapitalizeStatus.rotate();
748        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
749        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
750        mConnection.deleteSurroundingText(numCharsDeleted, 0);
751        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
752        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
753        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
754        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
755        // Match the keyboard to the new state.
756        keyboardSwitcher.updateShiftState();
757    }
758
759    /**
760     * Factor in auto-caps and manual caps and compute the current caps mode.
761     * @param keyboardSwitcher the keyboard switcher. Caps mode depends on its mode.
762     * @return the actual caps mode the keyboard is in right now.
763     */
764    // TODO: Make this private
765    public int getActualCapsMode(final KeyboardSwitcher keyboardSwitcher) {
766        final int keyboardShiftMode = keyboardSwitcher.getKeyboardShiftMode();
767        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
768        final int auto = mLatinIME.getCurrentAutoCapsState();
769        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
770            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
771        }
772        if (0 != auto) {
773            return WordComposer.CAPS_MODE_AUTO_SHIFTED;
774        }
775        return WordComposer.CAPS_MODE_OFF;
776    }
777
778    /**
779     * @return the editor info for the current editor
780     */
781    private EditorInfo getCurrentInputEditorInfo() {
782        return mLatinIME.getCurrentInputEditorInfo();
783    }
784
785    /**
786     * Get the nth previous word before the cursor as context for the suggestion process.
787     * @param currentSettings the current settings values.
788     * @param nthPreviousWord reverse index of the word to get (1-indexed)
789     * @return the nth previous word before the cursor.
790     */
791    // TODO: Make this private
792    public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
793            final int nthPreviousWord) {
794        if (currentSettings.mCurrentLanguageHasSpaces) {
795            // If we are typing in a language with spaces we can just look up the previous
796            // word from textview.
797            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
798        } else {
799            return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
800                    : mLastComposedWord.mCommittedWord;
801        }
802    }
803
804    /**
805     * @param actionId the action to perform
806     */
807    private void performEditorAction(final int actionId) {
808        mConnection.performEditorAction(actionId);
809    }
810
811    /**
812     * Handle a press on the settings key.
813     */
814    private void onSettingsKeyPressed() {
815        mLatinIME.onSettingsKeyPressed();
816    }
817
818    // This will reset the whole input state to the starting state. It will clear
819    // the composing word, reset the last composed word, tell the inputconnection about it.
820    // TODO: remove all references to this in LatinIME and make this private
821    public void resetEntireInputState(final SettingsValues settingsValues,
822            final int newSelStart, final int newSelEnd) {
823        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
824        resetComposingState(true /* alsoResetLastComposedWord */);
825        if (settingsValues.mBigramPredictionEnabled) {
826            mLatinIME.clearSuggestionStrip();
827        } else {
828            mLatinIME.setSuggestedWords(settingsValues.mSuggestPuncList, false);
829        }
830        mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
831                shouldFinishComposition);
832    }
833
834    // TODO: remove all references to this in LatinIME and make this private.
835    public void resetComposingState(final boolean alsoResetLastComposedWord) {
836        mWordComposer.reset();
837        if (alsoResetLastComposedWord) {
838            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
839        }
840    }
841
842    // TODO: remove all references to this in LatinIME and make this private. Also, shouldn't
843    // this go in some *Utils class instead?
844    public CharSequence getTextWithUnderline(final String text) {
845        return mIsAutoCorrectionIndicatorOn
846                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
847                : text;
848    }
849
850    private void sendDownUpKeyEvent(final int code) {
851        final long eventTime = SystemClock.uptimeMillis();
852        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
853                KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
854                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
855        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
856                KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
857                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
858    }
859
860    // TODO: remove all references to this in LatinIME and make this private
861    public void sendKeyCodePoint(final int code) {
862        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
863            ResearchLogger.latinIME_sendKeyCodePoint(code);
864        }
865        // TODO: Remove this special handling of digit letters.
866        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
867        if (code >= '0' && code <= '9') {
868            sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
869            return;
870        }
871
872        if (Constants.CODE_ENTER == code && mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean()) {
873            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
874            // a hardware keyboard event on pressing enter or delete. This is bad for many
875            // reasons (there are race conditions with commits) but some applications are
876            // relying on this behavior so we continue to support it for older apps.
877            sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
878        } else {
879            mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
880        }
881    }
882
883    // This essentially inserts a space, and that's it.
884    // TODO: Make this private.
885    public void promotePhantomSpace(final SettingsValues settingsValues) {
886        if (settingsValues.shouldInsertSpacesAutomatically()
887                && settingsValues.mCurrentLanguageHasSpaces
888                && !mConnection.textBeforeCursorLooksLikeURL()) {
889            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
890                ResearchLogger.latinIME_promotePhantomSpace();
891            }
892            sendKeyCodePoint(Constants.CODE_SPACE);
893        }
894    }
895
896    // TODO: Make this private
897    public void commitTyped(final String separatorString) {
898        if (!mWordComposer.isComposingWord()) return;
899        final String typedWord = mWordComposer.getTypedWord();
900        if (typedWord.length() > 0) {
901            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
902                ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
903            }
904            mLatinIME.commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
905                    separatorString);
906        }
907    }
908
909    // TODO: Make this private
910    public void commitCurrentAutoCorrection(final SettingsValues settingsValues,
911            final String separator,
912            // TODO: Remove this argument.
913            final LatinIME.UIHandler handler) {
914        // Complete any pending suggestions query first
915        if (handler.hasPendingUpdateSuggestions()) {
916            mLatinIME.updateSuggestionStrip();
917        }
918        final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
919        final String typedWord = mWordComposer.getTypedWord();
920        final String autoCorrection = (typedAutoCorrection != null)
921                ? typedAutoCorrection : typedWord;
922        if (autoCorrection != null) {
923            if (TextUtils.isEmpty(typedWord)) {
924                throw new RuntimeException("We have an auto-correction but the typed word "
925                        + "is empty? Impossible! I must commit suicide.");
926            }
927            if (settingsValues.mIsInternal) {
928                LatinImeLoggerUtils.onAutoCorrection(
929                        typedWord, autoCorrection, separator, mWordComposer);
930            }
931            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
932                final SuggestedWords suggestedWords = mSuggestedWords;
933                ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
934                        separator, mWordComposer.isBatchMode(), suggestedWords);
935            }
936            mLatinIME.commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
937                    separator);
938            if (!typedWord.equals(autoCorrection)) {
939                // This will make the correction flash for a short while as a visual clue
940                // to the user that auto-correction happened. It has no other effect; in particular
941                // note that this won't affect the text inside the text field AT ALL: it only makes
942                // the segment of text starting at the supplied index and running for the length
943                // of the auto-correction flash. At this moment, the "typedWord" argument is
944                // ignored by TextView.
945                mConnection.commitCorrection(
946                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
947                        typedWord, autoCorrection));
948            }
949        }
950    }
951}
952