diff options
Diffstat (limited to 'java/src')
4 files changed, 1491 insertions, 1067 deletions
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index c260434d5..202ac87ef 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -141,7 +141,7 @@ public final class Constants { } public static final int NOT_A_CODE = -1; - + public static final int NOT_A_CURSOR_POSITION = -1; public static final int NOT_A_COORDINATE = -1; public static final int SUGGESTION_STRIP_COORDINATE = -2; public static final int SPELL_CHECKER_COORDINATE = -3; @@ -154,6 +154,11 @@ public final class Constants { // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h public static final int DICTIONARY_MAX_WORD_LENGTH = 48; + // Key events coming any faster than this are long-presses. + public static final int LONG_PRESS_MILLISECONDS = 200; + // How many continuous deletes at which to start deleting at a higher speed. + public static final int DELETE_ACCELERATE_AT = 20; + public static boolean isValidCoordinate(final int coordinate) { // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE}, // and {@link SPELL_CHECKER_COORDINATE}. @@ -242,5 +247,4 @@ public final class Constants { private Constants() { // This utility class is not publicly instantiable. } - } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 67b570277..ab9b2e598 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -76,6 +76,8 @@ import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.inputlogic.InputLogic; +import com.android.inputmethod.latin.inputlogic.SpaceState; import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; @@ -115,15 +117,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen Suggest.SuggestInitializationListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; - private static boolean DEBUG; + // TODO[IL]: Make this private + public static boolean DEBUG; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; - // How many continuous deletes at which to start deleting at a higher speed. - private static final int DELETE_ACCELERATE_AT = 20; - // Key events coming any faster than this are long-presses. - private static final int QUICK_PRESS = 200; - private static final int PENDING_IMS_CALLBACK_DURATION = 800; private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; @@ -137,79 +135,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ private static final String SCHEME_PACKAGE = "package"; - private static final int SPACE_STATE_NONE = 0; - // Double space: the state where the user pressed space twice quickly, which LatinIME - // resolved as period-space. Undoing this converts the period to a space. - private static final int SPACE_STATE_DOUBLE = 1; - // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip - // have just been swapped. Undoing this swaps them back; the space is still considered weak. - private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; - // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak - // spaces happen when the user presses space, accepting the current suggestion (whether - // it's an auto-correction or not). - private static final int SPACE_STATE_WEAK = 3; - // Phantom space: a not-yet-inserted space that should get inserted on the next input, - // character provided it's not a separator. If it's a separator, the phantom space is dropped. - // Phantom spaces happen when a user chooses a word from the suggestion strip. - private static final int SPACE_STATE_PHANTOM = 4; - - // Current space state of the input method. This can be any of the above constants. - private int mSpaceState; - private final Settings mSettings; + private final InputLogic mInputLogic = new InputLogic(this); private View mExtractArea; private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; - // Never null - private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; - private Suggest mSuggest; + private CompletionInfo[] mApplicationSpecifiedCompletions; - private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); + // TODO[IL]: Make this an AsyncResultHolder or a Future in SettingsValues + public AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); - // At start, create a default event interpreter that does nothing by passing it no decoder spec. - // The event interpreter should never be null. - private EventInterpreter mEventInterpreter = new EventInterpreter(this); private boolean mIsMainDictionaryAvailable; private UserBinaryDictionary mUserDictionary; private boolean mIsUserDictionaryAvailable; - private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private final WordComposer mWordComposer = new WordComposer(); - private final RichInputConnection mConnection = new RichInputConnection(this); - private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); - - // Keep track of the last selection range to decide if we need to show word alternatives - private static final int NOT_A_CURSOR_POSITION = -1; - private int mLastSelectionStart = NOT_A_CURSOR_POSITION; - private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; - - private int mDeleteCount; - private long mLastKeyTime; - private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); // Personalization debugging params private boolean mUseOnlyPersonalizationDictionaryForDebug = false; private boolean mBoostPersonalizationDictionaryForDebug = false; - // Member variables for remembering the current device orientation. - private int mDisplayOrientation; + // Member variable for remembering the current device orientation. + // TODO[IL]: Move this to SettingsValues. + public int mDisplayOrientation; // Object for reacting to adding/removing a dictionary pack. private BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); - // Keeps track of most recently inserted text (multi-character key) for reverting - private String mEnteredText; - - // TODO: This boolean is persistent state and causes large side effects at unexpected times. - // Find a way to remove it for readability. - private boolean mIsAutoCorrectionIndicatorOn; - private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; @@ -547,7 +504,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen initSuggest(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); + ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest); } mDisplayOrientation = getResources().getConfiguration().orientation; @@ -588,9 +545,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // the layout; at this time, we need to skip resetting the contacts dictionary. It will // be done later inside {@see #initSuggest()} when the reopenDictionaries message is // processed. - if (!mHandler.hasPendingReopenDictionaries() && mSuggest != null) { + if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) { // May need to reset dictionaries depending on the user settings. - mSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent()); + mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, + mSettings.getCurrent()); } } @@ -636,24 +594,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mUserDictionary = new UserBinaryDictionary(this, subtypeLocale); mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); newSuggest.setUserDictionary(mUserDictionary); - newSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent()); - final Suggest oldSuggest = mSuggest; - mSuggest = newSuggest; + newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, + mSettings.getCurrent()); + final Suggest oldSuggest = mInputLogic.mSuggest; + mInputLogic.mSuggest = newSuggest; if (oldSuggest != null) oldSuggest.close(); } /* package private */ void resetSuggestMainDict() { final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); + mInputLogic.mSuggest.resetMainDict(this, subtypeLocale, + this /* SuggestInitializationListener */); mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); } @Override public void onDestroy() { - final Suggest suggest = mSuggest; + final Suggest suggest = mInputLogic.mSuggest; if (suggest != null) { suggest.close(); - mSuggest = null; + mInputLogic.mSuggest = null; } mSettings.onDestroy(); unregisterReceiver(mReceiver); @@ -676,10 +636,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (mDisplayOrientation != conf.orientation) { mDisplayOrientation = conf.orientation; mHandler.startOrientationChanging(); - mConnection.beginBatchEdit(); - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - mConnection.finishComposingText(); - mConnection.endBatchEdit(); + mInputLogic.mConnection.beginBatchEdit(); + mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.mConnection.finishComposingText(); + mInputLogic.mConnection.endBatchEdit(); if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); } @@ -823,16 +783,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. - mEnteredText = null; - resetComposingState(true /* alsoResetLastComposedWord */); - mDeleteCount = 0; - mSpaceState = SPACE_STATE_NONE; - mRecapitalizeStatus.deactivate(); - mCurrentlyPressedHardwareKeys.clear(); + mInputLogic.mEnteredText = null; + mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogic.mDeleteCount = 0; + mInputLogic.mSpaceState = SpaceState.NONE; + mInputLogic.mRecapitalizeStatus.deactivate(); + mInputLogic.mCurrentlyPressedHardwareKeys.clear(); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final Suggest suggest = mSuggest; + final Suggest suggest = mInputLogic.mSuggest; if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { initSuggest(); } @@ -841,14 +801,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // otherwise it will clear the suggestion strip. setPunctuationSuggestions(); } - mSuggestedWords = SuggestedWords.EMPTY; + mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; // Sometimes, while rotating, for some reason the framework tells the app we are not // connected to it and that means we can't refresh the cache. In this case, schedule a // refresh later. final boolean canReachInputConnection; - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart, - editorInfo.initialSelEnd, false /* shouldFinishComposition */)) { + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + editorInfo.initialSelStart, editorInfo.initialSelEnd, + false /* shouldFinishComposition */)) { // We try resetting the caches up to 5 times before giving up. mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); // mLastSelection{Start,End} are reset later in this method, don't need to do it here @@ -888,8 +849,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setSuggestionStripShownInternal( isSuggestionsStripVisible(), /* needsInputViewShown */ false); - mLastSelectionStart = editorInfo.initialSelStart; - mLastSelectionEnd = editorInfo.initialSelEnd; + mInputLogic.mLastSelectionStart = editorInfo.initialSelStart; + mInputLogic.mLastSelectionEnd = editorInfo.initialSelEnd; // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying // so we try using some heuristics to find out about these and fix them. tryFixLyingCursorPosition(); @@ -922,26 +883,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen * than it really is. */ private void tryFixLyingCursorPosition() { - final CharSequence textBeforeCursor = - mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + final CharSequence textBeforeCursor = mInputLogic.mConnection.getTextBeforeCursor( + Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); if (null == textBeforeCursor) { - mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION; + mInputLogic.mLastSelectionStart = mInputLogic.mLastSelectionEnd = + Constants.NOT_A_CURSOR_POSITION; } else { final int textLength = textBeforeCursor.length(); - if (textLength > mLastSelectionStart + if (textLength > mInputLogic.mLastSelectionStart || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE - && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { + && mInputLogic.mLastSelectionStart < + Constants.EDITOR_CONTENTS_CACHE_SIZE)) { // It should not be possible to have only one of those variables be // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized // (simple cursor, no selection) or there is no cursor/we don't know its pos - final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd; - mLastSelectionStart = textLength; + final boolean wasEqual = + mInputLogic.mLastSelectionStart == mInputLogic.mLastSelectionEnd; + mInputLogic.mLastSelectionStart = textLength; // We can't figure out the value of mLastSelectionEnd :( // But at least if it's smaller than mLastSelectionStart something is wrong, // and if they used to be equal we also don't want to make it look like there is a // selection. - if (wasEqual || mLastSelectionStart > mLastSelectionEnd) { - mLastSelectionEnd = mLastSelectionStart; + if (wasEqual || mInputLogic.mLastSelectionStart > mInputLogic.mLastSelectionEnd) { + mInputLogic.mLastSelectionEnd = mInputLogic.mLastSelectionStart; } } } @@ -998,12 +962,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( - if (mWordComposer.isComposingWord()) mConnection.finishComposingText(); - resetComposingState(true /* alsoResetLastComposedWord */); + if (mInputLogic.mWordComposer.isComposingWord()) { + mInputLogic.mConnection.finishComposingText(); + } + mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); // Notify ResearchLogger if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, - mLastSelectionEnd, getCurrentInputConnection()); + ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, + mInputLogic.mLastSelectionStart, + mInputLogic.mLastSelectionEnd, getCurrentInputConnection()); } } @@ -1016,21 +983,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (DEBUG) { Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd - + ", lss=" + mLastSelectionStart - + ", lse=" + mLastSelectionEnd + + ", lss=" + mInputLogic.mLastSelectionStart + + ", lse=" + mInputLogic.mLastSelectionEnd + ", nss=" + newSelStart + ", nse=" + newSelEnd + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, + ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart, + mInputLogic.mLastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, - composingSpanEnd, mConnection); + composingSpanEnd, mInputLogic.mConnection); } - final boolean selectionChanged = mLastSelectionStart != newSelStart - || mLastSelectionEnd != newSelEnd; + final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart + || mInputLogic.mLastSelectionEnd != newSelEnd; // if composingSpanStart and composingSpanEnd are -1, it means there is no composing // span in the view - we can use that to narrow down whether the cursor was moved @@ -1045,24 +1013,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: revisit this when LatinIME supports hardware keyboards. // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). // TODO: find a better way to simulate actual execution. - if (isInputViewShown() && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, - oldSelEnd, newSelEnd)) { + if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart, + newSelStart, oldSelEnd, newSelEnd)) { // TODO: the following is probably better done in resetEntireInputState(). // it should only happen when the cursor moved, and the very purpose of the // test below is to narrow down whether this happened or not. Likewise with // the call to updateShiftState. // We set this to NONE because after a cursor move, we don't want the space // state-related special processing to kick in. - mSpaceState = SPACE_STATE_NONE; + mInputLogic.mSpaceState = SpaceState.NONE; // TODO: is it still necessary to test for composingSpan related stuff? final boolean selectionChangedOrSafeToReset = selectionChanged - || (!mWordComposer.isComposingWord()) || noComposingSpan; + || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan; final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd); final int moveAmount = newSelStart - oldSelStart; if (selectionChangedOrSafeToReset && (hasOrHadSelection - || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { + || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord( + moveAmount))) { // If we are composing a word and moving the cursor, we would want to set a // suggestion span for recorrection to work correctly. Unfortunately, that // would involve the keyboard committing some new text, which would move the @@ -1072,14 +1041,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Another option would be to send suggestions each time we set the composing // text, but that is probably too expensive to do, so we decided to leave things // as is. - resetEntireInputState(newSelStart, newSelEnd); + mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd); } else { // resetEntireInputState calls resetCachesUponCursorMove, but forcing the // composition to end. But in all cases where we don't reset the entire input // state, we still want to tell the rich input connection about the new cursor // position so that it can update its caches. - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, - false /* shouldFinishComposition */); + mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + newSelStart, newSelEnd, false /* shouldFinishComposition */); } // We moved the cursor. If we are touching a word, we need to resume suggestion, @@ -1088,13 +1057,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.postResumeSuggestions(); } // Reset the last recapitalization. - mRecapitalizeStatus.deactivate(); + mInputLogic.mRecapitalizeStatus.deactivate(); mKeyboardSwitcher.updateShiftState(); } // Make a note of the cursor position - mLastSelectionStart = newSelStart; - mLastSelectionEnd = newSelEnd; + mInputLogic.mLastSelectionStart = newSelStart; + mInputLogic.mLastSelectionEnd = newSelEnd; mSubtypeState.currentSubtypeUsed(); } @@ -1299,42 +1268,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); } - // This will reset the whole input state to the starting state. It will clear - // the composing word, reset the last composed word, tell the inputconnection about it. - private void resetEntireInputState(final int newSelStart, final int newSelEnd) { - final boolean shouldFinishComposition = mWordComposer.isComposingWord(); - resetComposingState(true /* alsoResetLastComposedWord */); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.mBigramPredictionEnabled) { - clearSuggestionStrip(); - } else { - setSuggestedWords(settingsValues.mSuggestPuncList, false); - } - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, - shouldFinishComposition); - } - - private void resetComposingState(final boolean alsoResetLastComposedWord) { - mWordComposer.reset(); - if (alsoResetLastComposedWord) { - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - } - } - - private void commitTyped(final String separatorString) { - if (!mWordComposer.isComposingWord()) return; - final String typedWord = mWordComposer.getTypedWord(); - if (typedWord.length() > 0) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); - } - commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, - separatorString); - } - } - // Called from the KeyboardSwitcher which needs to know auto caps state to display // the right layout. + // TODO[IL]: Move this to InputLogic. public int getCurrentAutoCapsState() { final SettingsValues currentSettingsValues = mSettings.getCurrent(); if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; @@ -1344,96 +1280,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int inputType = ei.inputType; // Warning: this depends on mSpaceState, which may not be the most current value. If // mSpaceState gets updated later, whoever called this may need to be told about it. - return mConnection.getCursorCapsMode(inputType, currentSettingsValues, - SPACE_STATE_PHANTOM == mSpaceState); + return mInputLogic.mConnection.getCursorCapsMode(inputType, currentSettingsValues, + SpaceState.PHANTOM == mInputLogic.mSpaceState); } public int getCurrentRecapitalizeState() { - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + if (!mInputLogic.mRecapitalizeStatus.isActive() + || !mInputLogic.mRecapitalizeStatus.isSetAt(mInputLogic.mLastSelectionStart, + mInputLogic.mLastSelectionEnd)) { // Not recapitalizing at the moment return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; } - return mRecapitalizeStatus.getCurrentMode(); - } - - // Factor in auto-caps and manual caps and compute the current caps mode. - private int getActualCapsMode() { - final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); - if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; - final int auto = getCurrentAutoCapsState(); - if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { - return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; - } - if (0 != auto) { - return WordComposer.CAPS_MODE_AUTO_SHIFTED; - } - return WordComposer.CAPS_MODE_OFF; - } - - private void swapSwapperAndSpace() { - final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); - // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. - if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) { - mConnection.deleteSurroundingText(2, 0); - final String text = lastTwo.charAt(1) + " "; - mConnection.commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); - } - mKeyboardSwitcher.updateShiftState(); - } - } - - private boolean maybeDoubleSpacePeriod() { - final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (!currentSettingsValues.mUseDoubleSpacePeriod) return false; - if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; - // We only do this when we see two spaces and an accepted code point before the cursor. - // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. - final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); - if (null == lastThree) return false; - final int length = lastThree.length(); - if (length < 3) return false; - if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; - if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; - // We know there are spaces in pos -1 and -2, and we have at least three chars. - // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, - // so this is fine. - final int firstCodePoint = - Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? - Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); - if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { - mHandler.cancelDoubleSpacePeriodTimer(); - mConnection.deleteSurroundingText(2, 0); - final String textToInsert = new String( - new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE }, - 0, 2); - mConnection.commitText(textToInsert, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, - false /* isBatchMode */); - } - mWordComposer.discardPreviousWordForSuggestion(); - mKeyboardSwitcher.updateShiftState(); - return true; - } - return false; - } - - private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { - // TODO: Check again whether there really ain't a better way to check this. - // TODO: This should probably be language-dependant... - return Character.isLetterOrDigit(codePoint) - || codePoint == Constants.CODE_SINGLE_QUOTE - || codePoint == Constants.CODE_DOUBLE_QUOTE - || codePoint == Constants.CODE_CLOSING_PARENTHESIS - || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET - || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET - || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET - || codePoint == Constants.CODE_PLUS - || codePoint == Constants.CODE_PERCENT - || Character.getType(codePoint) == Character.OTHER_SYMBOL; + return mInputLogic.mRecapitalizeStatus.getCurrentMode(); } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is @@ -1445,7 +1303,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final String wordToEdit; - if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { + if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { wordToEdit = word; @@ -1453,7 +1311,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mUserDictionary.addWordToUserDictionary(wordToEdit); } - private void onSettingsKeyPressed() { + // TODO[IL]: Rework the route through which this is called. + public void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; showSubtypeSelectorAndSettings(); } @@ -1476,12 +1335,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mOptionsDialog != null && mOptionsDialog.isShowing(); } - private void performEditorAction(final int actionId) { - mConnection.performEditorAction(actionId); - } - // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. - private void handleLanguageSwitchKey() { + // TODO[IL]: Move a part of this to InputLogic and straighten out the interface for this. + public void handleLanguageSwitchKey() { final IBinder token = getWindow().getWindow().getAttributes().token; if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); @@ -1490,197 +1346,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeState.switchSubtype(token, mRichImm); } - private void sendDownUpKeyEvent(final int code) { - final long eventTime = SystemClock.uptimeMillis(); - mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, - KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, - KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - } - - private void sendKeyCodePoint(final int code) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_sendKeyCodePoint(code); - } - // TODO: Remove this special handling of digit letters. - // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. - if (code >= '0' && code <= '9') { - sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0); - return; - } - - if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) { - // Backward compatibility mode. Before Jelly bean, the keyboard would simulate - // a hardware keyboard event on pressing enter or delete. This is bad for many - // reasons (there are race conditions with commits) but some applications are - // relying on this behavior so we continue to support it for older apps. - sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); - } else { - mConnection.commitText(StringUtils.newSingleCodePointString(code), 1); - } - } - // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(final int primaryCode, final int x, final int y) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final long when = SystemClock.uptimeMillis(); - if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { - mDeleteCount = 0; - } - mLastKeyTime = when; - mConnection.beginBatchEdit(); - final KeyboardSwitcher switcher = mKeyboardSwitcher; - // The space state depends only on the last character pressed and its own previous - // state. Here, we revert the space state to neutral if the key is actually modifying - // the input contents (any non-shift key), which is what we should do for - // all inputs that do not result in a special state. Each character handling is then - // free to override the state as they see fit. - final int spaceState = mSpaceState; - if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; - - // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. - if (primaryCode != Constants.CODE_SPACE) { - mHandler.cancelDoubleSpacePeriodTimer(); - } - - boolean didAutoCorrect = false; - switch (primaryCode) { - case Constants.CODE_DELETE: - mSpaceState = SPACE_STATE_NONE; - handleBackspace(spaceState); - LatinImeLogger.logOnDelete(x, y); - break; - case Constants.CODE_SHIFT: - // Note: Calling back to the keyboard on Shift key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - final Keyboard currentKeyboard = switcher.getKeyboard(); - if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { - // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for - // alphabetic shift and shift while in symbol layout. - handleRecapitalize(); - } - break; - case Constants.CODE_CAPSLOCK: - // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. - break; - case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Note: Calling back to the keyboard on symbol key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - break; - case Constants.CODE_SETTINGS: - onSettingsKeyPressed(); - break; - case Constants.CODE_SHORTCUT: - mSubtypeSwitcher.switchToShortcutIME(this); - break; - case Constants.CODE_ACTION_NEXT: - performEditorAction(EditorInfo.IME_ACTION_NEXT); - break; - case Constants.CODE_ACTION_PREVIOUS: - performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); - break; - case Constants.CODE_LANGUAGE_SWITCH: - handleLanguageSwitchKey(); - break; - case Constants.CODE_EMOJI: - // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. - break; - case Constants.CODE_ENTER: - final EditorInfo editorInfo = getCurrentInputEditorInfo(); - final int imeOptionsActionId = - InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); - if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { - // Either we have an actionLabel and we should performEditorAction with actionId - // regardless of its value. - performEditorAction(editorInfo.actionId); - } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { - // We didn't have an actionLabel, but we had another action to execute. - // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, - // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it - // means there should be an action and the app didn't bother to set a specific - // code for it - presumably it only handles one. It does not have to be treated - // in any specific way: anything that is not IME_ACTION_NONE should be sent to - // performEditorAction. - performEditorAction(imeOptionsActionId); - } else { - // No action label, and the action from imeOptions is NONE: this is a regular - // enter key that should input a carriage return. - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); - } - break; - case Constants.CODE_SHIFT_ENTER: - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); - break; - default: - didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); - break; - } - switcher.onCodeInput(primaryCode); - // Reset after any single keystroke, except shift, capslock, and symbol-shift - if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT - && primaryCode != Constants.CODE_CAPSLOCK - && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) - mLastComposedWord.deactivate(); - if (Constants.CODE_DELETE != primaryCode) { - mEnteredText = null; - } - mConnection.endBatchEdit(); - } - - private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, - final int spaceState) { - mSpaceState = SPACE_STATE_NONE; - final boolean didAutoCorrect; - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.isWordSeparator(primaryCode) - || Character.getType(primaryCode) == Character.OTHER_SYMBOL) { - didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); - } else { - didAutoCorrect = false; - if (SPACE_STATE_PHANTOM == spaceState) { - if (settingsValues.mIsInternal) { - if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); - } - } - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart, mLastSelectionEnd); - } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - } - } - final int keyX, keyY; - final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { - keyX = x; - keyY = y; - } else { - keyX = Constants.NOT_A_COORDINATE; - keyY = Constants.NOT_A_COORDINATE; - } - handleCharacter(primaryCode, keyX, keyY, spaceState); - } - return didAutoCorrect; + mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher); } // Called from PointerTracker through the KeyboardActionListener interface + // TODO[IL]: Move this to InputLogic @Override public void onTextInput(final String rawText) { - mConnection.beginBatchEdit(); - if (mWordComposer.isComposingWord()) { - commitCurrentAutoCorrection(rawText); + mInputLogic.mConnection.beginBatchEdit(); + if (mInputLogic.mWordComposer.isComposingWord()) { + mInputLogic.commitCurrentAutoCorrection(mSettings.getCurrent(), rawText, mHandler); } else { - resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); } mHandler.postUpdateSuggestionStrip(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS @@ -1689,40 +1369,42 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final String text = specificTldProcessingOnTextInput(rawText); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); + if (SpaceState.PHANTOM == mInputLogic.mSpaceState) { + mInputLogic.promotePhantomSpace(mSettings.getCurrent()); } - mConnection.commitText(text, 1); + mInputLogic.mConnection.commitText(text, 1); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); } - mConnection.endBatchEdit(); + mInputLogic.mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_NONE; + mInputLogic.mSpaceState = SpaceState.NONE; mKeyboardSwitcher.updateShiftState(); mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); - mEnteredText = text; + mInputLogic.mEnteredText = text; } @Override public void onStartBatchInput() { mInputUpdater.onStartBatchInput(); mHandler.cancelUpdateSuggestionStrip(); - mConnection.beginBatchEdit(); + mInputLogic.mConnection.beginBatchEdit(); final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (mWordComposer.isComposingWord()) { + if (mInputLogic.mWordComposer.isComposingWord()) { if (currentSettingsValues.mIsInternal) { - if (mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); + if (mInputLogic.mWordComposer.isBatchMode()) { + LatinImeLoggerUtils.onAutoCorrection("", + mInputLogic.mWordComposer.getTypedWord(), " ", + mInputLogic.mWordComposer); } } - final int wordComposerSize = mWordComposer.size(); + final int wordComposerSize = mInputLogic.mWordComposer.size(); // Since isComposingWord() is true, the size is at least 1. - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + if (mInputLogic.mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { // If we are in the middle of a recorrection, we need to commit the recorrection // first so that we can insert the batch input at the current cursor position. - resetEntireInputState(mLastSelectionStart, mLastSelectionEnd); + mInputLogic.resetEntireInputState(currentSettingsValues, + mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd); } else if (wordComposerSize <= 1) { // We auto-correct the previous (typed, not gestured) string iff it's one character // long. The reason for this is, even in the middle of gesture typing, you'll still @@ -1731,17 +1413,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // tapping probably is that the word you intend to type is not in the dictionary, // so we do not attempt to correct, on the assumption that if that was a dictionary // word, the user would probably have gestured instead. - commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.commitCurrentAutoCorrection(currentSettingsValues, + LastComposedWord.NOT_A_SEPARATOR, mHandler); } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); } } - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + final int codePointBeforeCursor = mInputLogic.mConnection.getCodePointBeforeCursor(); if (Character.isLetterOrDigit(codePointBeforeCursor) || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { final boolean autoShiftHasBeenOverriden = mKeyboardSwitcher.getKeyboardShiftMode() != getCurrentAutoCapsState(); - mSpaceState = SPACE_STATE_PHANTOM; + mInputLogic.mSpaceState = SpaceState.PHANTOM; if (!autoShiftHasBeenOverriden) { // When we change the space state, we need to update the shift state of the // keyboard unless it has been overridden manually. This is happening for example @@ -1751,10 +1434,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.updateShiftState(); } } - mConnection.endBatchEdit(); - mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(getActualCapsMode(), + mInputLogic.mConnection.endBatchEdit(); + mInputLogic.mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + mInputLogic.getActualCapsMode(mKeyboardSwitcher), // Prev word is 1st word before cursor - getNthPreviousWordForSuggestion(currentSettingsValues, 1 /* nthPreviousWord */)); + mInputLogic.getNthPreviousWordForSuggestion(currentSettingsValues, + 1 /* nthPreviousWord */)); } static final class InputUpdater implements Handler.Callback { @@ -1857,7 +1542,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // be synchronized. private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); + mLatinIme.mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, sequenceNumber, new OnGetSuggestedWordsCallback() { @Override @@ -1916,19 +1601,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onUpdateBatchInput(final InputPointers batchPointers) { if (mSettings.getCurrent().mPhraseGestureEnabled) { - final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); + final SuggestedWordInfo candidate = + mInputLogic.mSuggestedWords.getAutoCommitCandidate(); // If these suggested words have been generated with out of date input pointers, then // we skip auto-commit (see comments above on the mSequenceNumber member). - if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { + if (null != candidate + && mInputLogic.mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { if (candidate.mSourceDict.shouldAutoCommit(candidate)) { final String[] commitParts = candidate.mWord.split(" ", 2); batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); - promotePhantomSpace(); - mConnection.commitText(commitParts[0], 0); - mSpaceState = SPACE_STATE_PHANTOM; + mInputLogic.promotePhantomSpace(mSettings.getCurrent()); + mInputLogic.mConnection.commitText(commitParts[0], 0); + mInputLogic.mSpaceState = SpaceState.PHANTOM; mKeyboardSwitcher.updateShiftState(); - mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( - getActualCapsMode(), commitParts[0]); + mInputLogic.mWordComposer. + setCapitalizedModeAndPreviousWordAtStartComposingTime( + mInputLogic.getActualCapsMode(mKeyboardSwitcher), commitParts[0]); ++mAutoCommitSequenceNumber; } } @@ -1942,30 +1630,31 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (TextUtils.isEmpty(batchInputText)) { return; } - mConnection.beginBatchEdit(); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); + mInputLogic.mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mInputLogic.mSpaceState) { + mInputLogic.promotePhantomSpace(mSettings.getCurrent()); } if (mSettings.getCurrent().mPhraseGestureEnabled) { // Find the last space final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; if (0 != indexOfLastSpace) { - mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + mInputLogic.mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), + 1); showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); } final String lastWord = batchInputText.substring(indexOfLastSpace); - mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); + mInputLogic.mWordComposer.setBatchInputWord(lastWord); + mInputLogic.mConnection.setComposingText(lastWord, 1); } else { - mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); + mInputLogic.mWordComposer.setBatchInputWord(batchInputText); + mInputLogic.mConnection.setComposingText(batchInputText, 1); } - mConnection.endBatchEdit(); + mInputLogic.mConnection.endBatchEdit(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); } // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; + mInputLogic.mSpaceState = SpaceState.PHANTOM; mKeyboardSwitcher.updateShiftState(); } @@ -1982,9 +1671,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // We have a TLD (or something that looks like this): make sure we don't add // a space even if currently in phantom mode. - mSpaceState = SPACE_STATE_NONE; + mInputLogic.mSpaceState = SpaceState.NONE; // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code - final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); + final CharSequence lastOne = mInputLogic.mConnection.getTextBeforeCursor(1, 0); if (lastOne != null && lastOne.length() == 1 && lastOne.charAt(0) == Constants.CODE_PERIOD) { return text.substring(1); @@ -2012,378 +1701,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mInputUpdater.onCancelBatchInput(); } - private void handleBackspace(final int spaceState) { - mDeleteCount++; - - // In many cases, we may have to put the keyboard in auto-shift state again. However - // we want to wait a few milliseconds before doing it to avoid the keyboard flashing - // during key repeat. - mHandler.postUpdateShiftState(); - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can remove the character at the current cursor position. - resetEntireInputState(mLastSelectionStart, mLastSelectionEnd); - // When we exit this if-clause, mWordComposer.isComposingWord() will return false. - } - if (mWordComposer.isComposingWord()) { - if (mWordComposer.isBatchMode()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final String word = mWordComposer.getTypedWord(); - ResearchLogger.latinIME_handleBackspace_batch(word, 1); - } - final String rejectedSuggestion = mWordComposer.getTypedWord(); - mWordComposer.reset(); - mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); - } else { - mWordComposer.deleteLast(); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - mHandler.postUpdateSuggestionStrip(); - if (!mWordComposer.isComposingWord()) { - // If we just removed the last character, auto-caps mode may have changed so we - // need to re-evaluate. - mKeyboardSwitcher.updateShiftState(); - } - } else { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mLastComposedWord.canRevertCommit()) { - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onAutoCorrectionCancellation(); - } - revertCommit(); - return; - } - if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { - // Cancel multi-character input: remove the text we just entered. - // This is triggered on backspace after a key that inputs multiple characters, - // like the smiley key or the .com key. - mConnection.deleteSurroundingText(mEnteredText.length(), 0); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); - } - mEnteredText = null; - // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. - // In addition we know that spaceState is false, and that we should not be - // reverting any autocorrect at this point. So we can safely return. - return; - } - if (SPACE_STATE_DOUBLE == spaceState) { - mHandler.cancelDoubleSpacePeriodTimer(); - if (mConnection.revertDoubleSpacePeriod()) { - // No need to reset mSpaceState, it has already be done (that's why we - // receive it as a parameter) - return; - } - } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - if (mConnection.revertSwapPunctuation()) { - // Likewise - return; - } - } - - // No cancelling of commit/double space/swap: we have a regular backspace. - // We should backspace one char and restart suggestion if at the end of a word. - if (mLastSelectionStart != mLastSelectionEnd) { - // If there is a selection, remove it. - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to - // happen, and if it's wrong, the next call to onUpdateSelection will correct it, - // but we want to set it right away to avoid it being used with the wrong values - // later (typically, in a subsequent press on backspace). - mLastSelectionEnd = mLastSelectionStart; - mConnection.deleteSurroundingText(numCharsDeleted, 0); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace(numCharsDeleted, - false /* shouldUncommitLogUnit */); - } - } else { - // There is no selection, just delete one character. - if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { - // This should never happen. - Log.e(TAG, "Backspace when we don't know the selection position"); - } - if (mAppWorkAroundsUtils.isBeforeJellyBean() || - currentSettings.mInputAttributes.isTypeNull()) { - // There are two possible reasons to send a key event: either the field has - // type TYPE_NULL, in which case the keyboard should send events, or we are - // running in backward compatibility mode. Before Jelly bean, the keyboard - // would simulate a hardware keyboard event on pressing enter or delete. This - // is bad for many reasons (there are race conditions with commits) but some - // applications are relying on this behavior so we continue to support it for - // older apps, so we retain this behavior if the app has target SDK < JellyBean. - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - if (mDeleteCount > DELETE_ACCELERATE_AT) { - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - } - } else { - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursor == Constants.NOT_A_CODE) { - // Nothing to delete before the cursor. - return; - } - final int lengthToDelete = - Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDelete, 0); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace(lengthToDelete, - true /* shouldUncommitLogUnit */); - } - if (mDeleteCount > DELETE_ACCELERATE_AT) { - final int codePointBeforeCursorToDeleteAgain = - mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { - final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( - codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain, - true /* shouldUncommitLogUnit */); - } - } - } - } - } - if (currentSettings.isSuggestionsRequested(mDisplayOrientation) - && currentSettings.mCurrentLanguageHasSpaces) { - restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); - } - // We just removed a character. We need to update the auto-caps state. - mKeyboardSwitcher.updateShiftState(); - } - } - - /* - * Strip a trailing space if necessary and returns whether it's a swap weak space situation. - */ - private boolean maybeStripSpace(final int code, final int spaceState, - final boolean isFromSuggestionStrip) { - if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - mConnection.removeTrailingSpace(); - return false; - } - if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) - && isFromSuggestionStrip) { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.isUsuallyPrecededBySpace(code)) return false; - if (currentSettings.isUsuallyFollowedBySpace(code)) return true; - mConnection.removeTrailingSpace(); - } - return false; - } - - private void handleCharacter(final int primaryCode, final int x, final int y, - final int spaceState) { - // TODO: refactor this method to stop flipping isComposingWord around all the time, and - // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter - // which has the same name as other handle* methods but is not the same. - boolean isComposingWord = mWordComposer.isComposingWord(); - - // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. - // See onStartBatchInput() to see how to do it. - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) { - if (isComposingWord) { - // Sanity check - throw new RuntimeException("Should not be composing here"); - } - promotePhantomSpace(); - } - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart, mLastSelectionEnd); - isComposingWord = false; - } - // We want to find out whether to start composing a new word with this character. If so, - // we need to reset the composing state and switch isComposingWord. The order of the - // tests is important for good performance. - // We only start composing if we're not already composing. - if (!isComposingWord - // We only start composing if this is a word code point. Essentially that means it's a - // a letter or a word connector. - && currentSettings.isWordCodePoint(primaryCode) - // We never go into composing state if suggestions are not requested. - && currentSettings.isSuggestionsRequested(mDisplayOrientation) && - // In languages with spaces, we only start composing a word when we are not already - // touching a word. In languages without spaces, the above conditions are sufficient. - (!mConnection.isCursorTouchingWord(currentSettings) - || !currentSettings.mCurrentLanguageHasSpaces)) { - // Reset entirely the composing state anyway, then start composing a new word unless - // the character is a single quote or a dash. The idea here is, single quote and dash - // are not separators and they should be treated as normal characters, except in the - // first position where they should not start composing a word. - isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode - && Constants.CODE_DASH != primaryCode); - // Here we don't need to reset the last composed word. It will be reset - // when we commit this one, if we ever do; if on the other hand we backspace - // it entirely and resume suggestions on the previous word, we'd like to still - // have touch coordinates for it. - resetComposingState(false /* alsoResetLastComposedWord */); - } - if (isComposingWord) { - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - // TODO: We should reconsider which coordinate system should be used to represent - // keyboard event. - final int keyX = mainKeyboardView.getKeyX(x); - final int keyY = mainKeyboardView.getKeyY(y); - mWordComposer.add(primaryCode, keyX, keyY); - // If it's the first letter, make note of auto-caps state - if (mWordComposer.size() == 1) { - // We pass 1 to getPreviousWordForSuggestion because we were not composing a word - // yet, so the word we want is the 1st word before the cursor. - mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( - getActualCapsMode(), - getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord */)); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - } else { - final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, - Constants.SUGGESTION_STRIP_COORDINATE == x); - - sendKeyCodePoint(primaryCode); - - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_WEAK; - } - // In case the "add to dictionary" hint was still displayed. - if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); - } - mHandler.postUpdateSuggestionStrip(); - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y); - } - } - - private void handleRecapitalize() { - if (mLastSelectionStart == mLastSelectionEnd) return; // No selection - // If we have a recapitalize in progress, use it; otherwise, create a new one. - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - final CharSequence selectedText = - mConnection.getSelectedText(0 /* flags, 0 for no styles */); - if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection - final SettingsValues currentSettings = mSettings.getCurrent(); - mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, - selectedText.toString(), currentSettings.mLocale, - currentSettings.mWordSeparators); - // We trim leading and trailing whitespace. - mRecapitalizeStatus.trim(); - // Trimming the object may have changed the length of the string, and we need to - // reposition the selection handles accordingly. As this result in an IPC call, - // only do it if it's actually necessary, in other words if the recapitalize status - // is not set at the same place as before. - if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - } - } - mConnection.finishComposingText(); - mRecapitalizeStatus.rotate(); - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - mConnection.deleteSurroundingText(numCharsDeleted, 0); - mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); - // Match the keyboard to the new state. - mKeyboardSwitcher.updateShiftState(); - } - - // Returns true if we do an autocorrection, false otherwise. - private boolean handleSeparator(final int primaryCode, final int x, final int y, - final int spaceState) { - boolean didAutoCorrect = false; - final SettingsValues currentSettings = mSettings.getCurrent(); - // We avoid sending spaces in languages without spaces if we were composing. - final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode - && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord(); - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the separator at the current cursor position. - resetEntireInputState(mLastSelectionStart, mLastSelectionEnd); - } - if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing - if (currentSettings.mCorrectionEnabled) { - final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR - : StringUtils.newSingleCodePointString(primaryCode); - commitCurrentAutoCorrection(separator); - didAutoCorrect = true; - } else { - commitTyped(StringUtils.newSingleCodePointString(primaryCode)); - } - } - - final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, - Constants.SUGGESTION_STRIP_COORDINATE == x); - - if (SPACE_STATE_PHANTOM == spaceState && - currentSettings.isUsuallyPrecededBySpace(primaryCode)) { - promotePhantomSpace(); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); - } - - if (!shouldAvoidSendingCode) { - sendKeyCodePoint(primaryCode); - } - - if (Constants.CODE_SPACE == primaryCode) { - if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (maybeDoubleSpacePeriod()) { - mSpaceState = SPACE_STATE_DOUBLE; - } else if (!isShowingPunctuationList()) { - mSpaceState = SPACE_STATE_WEAK; - } - } - - mHandler.startDoubleSpacePeriodTimer(); - mHandler.postUpdateSuggestionStrip(); - } else { - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; - } else if (SPACE_STATE_PHANTOM == spaceState - && currentSettings.isUsuallyFollowedBySpace(primaryCode)) { - // If we are in phantom space state, and the user presses a separator, we want to - // stay in phantom space state so that the next keypress has a chance to add the - // space. For example, if I type "Good dat", pick "day" from the suggestion strip - // then insert a comma and go on to typing the next word, I want the space to be - // inserted automatically before the next word, the same way it is when I don't - // input the comma. - // The case is a little different if the separator is a space stripper. Such a - // separator does not normally need a space on the right (that's the difference - // between swappers and strippers), so we should not stay in phantom space state if - // the separator is a stripper. Hence the additional test above. - mSpaceState = SPACE_STATE_PHANTOM; - } - - // Set punctuation right away. onUpdateSelection will fire but tests whether it is - // already displayed or not, so it's okay. - setPunctuationSuggestions(); - } - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y); - } - - mKeyboardSwitcher.updateShiftState(); - return didAutoCorrect; - } - - private CharSequence getTextWithUnderline(final String text) { - return mIsAutoCorrectionIndicatorOn - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) - : text; - } - + // TODO[IL]: Rename this to avoid using handle* private void handleClose() { // TODO: Verify that words are logged properly when IME is closed. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); requestHideSelf(0); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { @@ -2391,12 +1712,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - // TODO: make this private + // TODO[IL]: Move this to InputLogic and make it private // Outside LatinIME, only used by the test suite. @UsedForTesting - boolean isShowingPunctuationList() { - if (mSuggestedWords == null) return false; - return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; + public boolean isShowingPunctuationList() { + if (mInputLogic.mSuggestedWords == null) return false; + return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords; } private boolean isSuggestionsStripVisible() { @@ -2414,13 +1735,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return currentSettings.isSuggestionsRequested(mDisplayOrientation); } - private void clearSuggestionStrip() { + public void dismissAddToDictionaryHint() { + if (null != mSuggestionStripView) { + mSuggestionStripView.dismissAddToDictionaryHint(); + } + } + + // TODO[IL]: Define a clear interface for this + public void clearSuggestionStrip() { setSuggestedWords(SuggestedWords.EMPTY, false); setAutoCorrectionIndicator(false); } - private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { - mSuggestedWords = words; + // TODO[IL]: Define a clear interface for this + public void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { + mInputLogic.mSuggestedWords = words; if (mSuggestionStripView != null) { mSuggestionStripView.setSuggestions(words); mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); @@ -2429,34 +1758,36 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { // Put a blue underline to a word in TextView which will be auto-corrected. - if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator - && mWordComposer.isComposingWord()) { - mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; + if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator + && mInputLogic.mWordComposer.isComposingWord()) { + mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; final CharSequence textWithUnderline = - getTextWithUnderline(mWordComposer.getTypedWord()); + mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord()); // TODO: when called from an updateSuggestionStrip() call that results from a posted // message, this is called outside any batch edit. Potentially, this may result in some // janky flickering of the screen, although the display speed makes it unlikely in // the practice. - mConnection.setComposingText(textWithUnderline, 1); + mInputLogic.mConnection.setComposingText(textWithUnderline, 1); } } - private void updateSuggestionStrip() { + // TODO[IL]: Move this to InputLogic and make private again + public void updateSuggestionStrip() { mHandler.cancelUpdateSuggestionStrip(); final SettingsValues currentSettings = mSettings.getCurrent(); // Check if we have a suggestion engine attached. - if (mSuggest == null + if (mInputLogic.mSuggest == null || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (mWordComposer.isComposingWord()) { + if (mInputLogic.mWordComposer.isComposingWord()) { Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " + "requested!"); } return; } - if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) { + if (!mInputLogic.mWordComposer.isComposingWord() + && !currentSettings.mBigramPredictionEnabled) { setPunctuationSuggestions(); return; } @@ -2478,28 +1809,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - /** - * Get the nth previous word before the cursor as context for the suggestion process. - * @param currentSettings the current settings values. - * @param nthPreviousWord reverse index of the word to get (1-indexed) - * @return the nth previous word before the cursor. - */ - private String getNthPreviousWordForSuggestion(final SettingsValues currentSettings, - final int nthPreviousWord) { - if (currentSettings.mCurrentLanguageHasSpaces) { - // If we are typing in a language with spaces we can just look up the previous - // word from textview. - return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord); - } else { - return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null - : mLastComposedWord.mCommittedWord; - } - } - private void getSuggestedWords(final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - final Suggest suggest = mSuggest; + final Suggest suggest = mInputLogic.mSuggest; if (keyboard == null || suggest == null) { callback.onGetSuggestedWords(SuggestedWords.EMPTY); return; @@ -2511,22 +1824,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; if (DEBUG) { - if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) { - final String previousWord = mWordComposer.getPreviousWordForSuggestion(); + if (mInputLogic.mWordComposer.isComposingWord() + || mInputLogic.mWordComposer.isBatchMode()) { + final String previousWord + = mInputLogic.mWordComposer.getPreviousWordForSuggestion(); // TODO: this is for checking consistency with older versions. Remove this when // we are confident this is stable. // We're checking the previous word in the text field against the memorized previous // word. If we are composing a word we should have the second word before the cursor // memorized, otherwise we should have the first. - final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings, - mWordComposer.isComposingWord() ? 2 : 1); + final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion( + currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1); if (!TextUtils.equals(previousWord, rereadPrevWord)) { throw new RuntimeException("Unexpected previous word: " + previousWord + " <> " + rereadPrevWord); } } } - suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWordForSuggestion(), + suggest.getSuggestedWords(mInputLogic.mWordComposer, + mInputLogic.mWordComposer.getPreviousWordForSuggestion(), keyboard.getProximityInfo(), currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, additionalFeaturesOptions, sessionId, sequenceNumber, callback); @@ -2539,7 +1855,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onGetSuggestedWords(SuggestedWords suggestedWords) { callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( - mWordComposer.getTypedWord(), suggestedWords)); + mInputLogic.mWordComposer.getTypedWord(), suggestedWords)); } }); } @@ -2563,7 +1879,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private SuggestedWords getOlderSuggestions(final String typedWord) { - SuggestedWords previousSuggestedWords = mSuggestedWords; + SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords; if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { previousSuggestedWords = SuggestedWords.EMPTY; } @@ -2591,7 +1907,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // because it may differ from mWordComposer.mTypedWord. autoCorrection = typedWord; } - mWordComposer.setAutoCorrection(autoCorrection); + mInputLogic.mWordComposer.setAutoCorrection(autoCorrection); } private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, @@ -2621,50 +1937,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); } - private void commitCurrentAutoCorrection(final String separator) { - // Complete any pending suggestions query first - if (mHandler.hasPendingUpdateSuggestions()) { - updateSuggestionStrip(); - } - final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); - final String typedWord = mWordComposer.getTypedWord(); - final String autoCorrection = (typedAutoCorrection != null) - ? typedAutoCorrection : typedWord; - if (autoCorrection != null) { - if (TextUtils.isEmpty(typedWord)) { - throw new RuntimeException("We have an auto-correction but the typed word " - + "is empty? Impossible! I must commit suicide."); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onAutoCorrection( - typedWord, autoCorrection, separator, mWordComposer); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final SuggestedWords suggestedWords = mSuggestedWords; - ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, - separator, mWordComposer.isBatchMode(), suggestedWords); - } - commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, - separator); - if (!typedWord.equals(autoCorrection)) { - // This will make the correction flash for a short while as a visual clue - // to the user that auto-correction happened. It has no other effect; in particular - // note that this won't affect the text inside the text field AT ALL: it only makes - // the segment of text starting at the supplied index and running for the length - // of the auto-correction flash. At this moment, the "typedWord" argument is - // ignored by TextView. - mConnection.commitCorrection( - new CorrectionInfo(mLastSelectionEnd - typedWord.length(), - typedWord, autoCorrection)); - } - } - } - // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { - final SuggestedWords suggestedWords = mSuggestedWords; + final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords; final String suggestion = suggestionInfo.mWord; // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && isShowingPunctuationList()) { @@ -2682,57 +1959,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } - mConnection.beginBatchEdit(); + mInputLogic.mConnection.beginBatchEdit(); final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 + if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0 // In the batch input mode, a manually picked suggested word should just replace // the current batch input text and there is no need for a phantom space. - && !mWordComposer.isBatchMode()) { + && !mInputLogic.mWordComposer.isBatchMode()) { final int firstChar = Character.codePointAt(suggestion, 0); if (!currentSettings.isWordSeparator(firstChar) || currentSettings.isUsuallyPrecededBySpace(firstChar)) { - promotePhantomSpace(); + mInputLogic.promotePhantomSpace(currentSettings); } } if (currentSettings.isApplicationSpecifiedCompletionsOn() && mApplicationSpecifiedCompletions != null && index >= 0 && index < mApplicationSpecifiedCompletions.length) { - mSuggestedWords = SuggestedWords.EMPTY; + mInputLogic.mSuggestedWords = SuggestedWords.EMPTY; if (mSuggestionStripView != null) { mSuggestionStripView.clear(); } mKeyboardSwitcher.updateShiftState(); - resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */); final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; - mConnection.commitCompletion(completionInfo); - mConnection.endBatchEdit(); + mInputLogic.mConnection.commitCompletion(completionInfo); + mInputLogic.mConnection.endBatchEdit(); return; } // We need to log before we commit, because the word composer will store away the user // typed word. - final String replacedWord = mWordComposer.getTypedWord(); + final String replacedWord = mInputLogic.mWordComposer.getTypedWord(); LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, - mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, - suggestionInfo.mSourceDict.mDictType); + mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore, + suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); } - mConnection.endBatchEdit(); + mInputLogic.mConnection.endBatchEdit(); // Don't allow cancellation of manual pick - mLastComposedWord.deactivate(); + mInputLogic.mLastComposedWord.deactivate(); // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; + mInputLogic.mSpaceState = SpaceState.PHANTOM; mKeyboardSwitcher.updateShiftState(); // We should show the "Touch again to save" hint if the user pressed the first entry // AND it's in none of our current dictionaries (main, user or otherwise). // Please note that if mSuggest is null, it means that everything is off: suggestion // and correction, so we shouldn't try to show the hint - final Suggest suggest = mSuggest; + final Suggest suggest = mInputLogic.mSuggest; final boolean showingAddToDictionaryHint = (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) @@ -2756,10 +2033,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen /** * Commits the chosen word to the text field and saves it for later retrieval. */ - private void commitChosenWord(final String chosenWord, final int commitType, + // TODO[IL]: Move to InputLogic and make public again + public void commitChosenWord(final String chosenWord, final int commitType, final String separatorString) { - final SuggestedWords suggestedWords = mSuggestedWords; - mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( + final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords; + mInputLogic.mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); // Add the word to the user history dictionary final String prevWord = addToUserHistoryDictionary(chosenWord); @@ -2767,8 +2045,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // what user typed. Note: currently this is done much later in // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. - mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString, - prevWord); + mInputLogic.mLastComposedWord = mInputLogic.mWordComposer.commitWord(commitType, + chosenWord, separatorString, prevWord); final boolean shouldDiscardPreviousWordForSuggestion; if (0 == StringUtils.codePointCount(separatorString)) { // Separator is 0-length. Discard the word only if the current language has spaces. @@ -2780,11 +2058,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen !StringUtils.containsOnlyWhitespace(separatorString); } if (shouldDiscardPreviousWordForSuggestion) { - mWordComposer.discardPreviousWordForSuggestion(); + mInputLogic.mWordComposer.discardPreviousWordForSuggestion(); } } - private void setPunctuationSuggestions() { + // TODO[IL]: Define a clean interface for this + public void setPunctuationSuggestions() { final SettingsValues currentSettings = mSettings.getCurrent(); if (currentSettings.mBigramPredictionEnabled) { clearSuggestionStrip(); @@ -2797,7 +2076,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private String addToUserHistoryDictionary(final String suggestion) { if (TextUtils.isEmpty(suggestion)) return null; - final Suggest suggest = mSuggest; + final Suggest suggest = mInputLogic.mSuggest; if (suggest == null) return null; // If correction is not enabled, we don't add words to the user history dictionary. @@ -2809,9 +2088,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary(); if (userHistoryDictionary == null) return null; - final String prevWord = mConnection.getNthPreviousWord(currentSettings, 2); + final String prevWord = mInputLogic.mConnection.getNthPreviousWord(currentSettings, 2); final String secondWord; - if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { + if (mInputLogic.mWordComposer.wasAutoCapitalized() + && !mInputLogic.mWordComposer.isMostlyCaps()) { secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); } else { secondWord = suggestion; @@ -2848,19 +2128,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // how to segment them yet. if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; // If the cursor is not touching a word, or if there is a selection, return right away. - if (mLastSelectionStart != mLastSelectionEnd) return; + if (mInputLogic.mLastSelectionStart != mInputLogic.mLastSelectionEnd) return; // If we don't know the cursor location, return. - if (mLastSelectionStart < 0) return; + if (mInputLogic.mLastSelectionStart < 0) return; final SettingsValues currentSettings = mSettings.getCurrent(); - if (!mConnection.isCursorTouchingWord(currentSettings)) return; - final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators, - 0 /* additionalPrecedingWordsCount */); + if (!mInputLogic.mConnection.isCursorTouchingWord(currentSettings)) return; + final TextRange range = mInputLogic.mConnection.getWordRangeAtCursor( + currentSettings.mWordSeparators, 0 /* additionalPrecedingWordsCount */); if (null == range) return; // Happens if we don't have an input connection at all if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. // If for some strange reason (editor bug or so) we measure the text before the cursor as // longer than what the entire text is supposed to be, the safe thing to do is bail out. final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); - if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return; + if (numberOfCharsInWordBeforeCursor > mInputLogic.mLastSelectionStart) return; final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); final String typedWord = range.mWord.toString(); if (!isResumableWord(typedWord, currentSettings)) return; @@ -2878,18 +2158,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - mWordComposer.setComposingWord(typedWord, - getNthPreviousWordForSuggestion(currentSettings, + mInputLogic.mWordComposer.setComposingWord(typedWord, + mInputLogic.getNthPreviousWordForSuggestion(currentSettings, // We want the previous word for suggestion. If we have chars in the word // before the cursor, then we want the word before that, hence 2; otherwise, // we want the word immediately before the cursor, hence 1. 0 == numberOfCharsInWordBeforeCursor ? 1 : 2), mKeyboardSwitcher.getKeyboard()); - mWordComposer.setCursorPositionWithinWord( + mInputLogic.mWordComposer.setCursorPositionWithinWord( typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); - mConnection.setComposingRegion( - mLastSelectionStart - numberOfCharsInWordBeforeCursor, - mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); + mInputLogic.mConnection.setComposingRegion( + mInputLogic.mLastSelectionStart - numberOfCharsInWordBeforeCursor, + mInputLogic.mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); if (suggestions.isEmpty()) { // We come here if there weren't any suggestion spans on this word. We will try to // compute suggestions for it instead. @@ -2938,42 +2218,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching // the text to adapt it. // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) - mIsAutoCorrectionIndicatorOn = false; + mInputLogic.mIsAutoCorrectionIndicatorOn = false; mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); } /** - * Check if the cursor is actually at the end of a word. If so, restart suggestions on this - * word, else do nothing. - */ - private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { - final CharSequence word = - mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); - if (null != word) { - final String wordString = word.toString(); - restartSuggestionsOnWordBeforeCursor(wordString); - // TODO: Handle the case where the user manually moves the cursor and then backs up over - // a separator. In that case, the current log unit should not be uncommitted. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, - true /* dumpCurrentLogUnit */); - } - } - } - - private void restartSuggestionsOnWordBeforeCursor(final String word) { - mWordComposer.setComposingWord(word, - // Previous word is the 2nd word before cursor because we are restarting on the - // 1st word before cursor. - getNthPreviousWordForSuggestion(mSettings.getCurrent(), 2 /* nthPreviousWord */), - mKeyboardSwitcher.getKeyboard()); - final int length = word.length(); - mConnection.deleteSurroundingText(length, 0); - mConnection.setComposingText(word, 1); - mHandler.postUpdateSuggestionStrip(); - } - - /** * Retry resetting caches in the rich input connection. * * When the editor can't be accessed we can't reset the caches, so we schedule a retry. @@ -2984,8 +2233,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen * @param remainingTries How many times we may try again before giving up. */ private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, - mLastSelectionEnd, false)) { + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd, false)) { if (0 < remainingTries) { mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1); return; @@ -2998,74 +2247,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (tryResumeSuggestions) mHandler.postResumeSuggestions(); } - private void revertCommit() { - final String previousWord = mLastComposedWord.mPrevWord; - final String originallyTypedWord = mLastComposedWord.mTypedWord; - final String committedWord = mLastComposedWord.mCommittedWord; - final int cancelLength = committedWord.length(); - // We want java chars, not codepoints for the following. - final int separatorLength = mLastComposedWord.mSeparatorString.length(); - // TODO: should we check our saved separator against the actual contents of the text view? - final int deleteLength = cancelLength + separatorLength; - if (DEBUG) { - if (mWordComposer.isComposingWord()) { - throw new RuntimeException("revertCommit, but we are composing a word"); - } - final CharSequence wordBeforeCursor = - mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); - if (!TextUtils.equals(committedWord, wordBeforeCursor)) { - throw new RuntimeException("revertCommit check failed: we thought we were " - + "reverting \"" + committedWord - + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); - } - } - mConnection.deleteSurroundingText(deleteLength, 0); - if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { - if (mSuggest != null) { - mSuggest.cancelAddingUserHistory(previousWord, committedWord); - } - } - final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; - if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { - // For languages with spaces, we revert to the typed string, but the cursor is still - // after the separator so we don't resume suggestions. If the user wants to correct - // the word, they have to press backspace again. - mConnection.commitText(stringToCommit, 1); - } else { - // For languages without spaces, we revert the typed string but the cursor is flush - // with the typed word, so we need to resume suggestions right away. - mWordComposer.setComposingWord(stringToCommit, previousWord, - mKeyboardSwitcher.getKeyboard()); - mConnection.setComposingText(stringToCommit, 1); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, - mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); - } - // Don't restart suggestion yet. We'll restart if the user deletes the - // separator. - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // We have a separator between the word and the cursor: we should show predictions. - mHandler.postUpdateSuggestionStrip(); - } - - // This essentially inserts a space, and that's it. - public void promotePhantomSpace() { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.shouldInsertSpacesAutomatically() - && currentSettings.mCurrentLanguageHasSpaces - && !mConnection.textBeforeCursorLooksLikeURL()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_promotePhantomSpace(); - } - sendKeyCodePoint(Constants.CODE_SPACE); - } - } - // TODO: Make this private // Outside LatinIME, only used by the {@link InputTestsBase} test suite. @UsedForTesting @@ -3090,7 +2271,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } if (repeatCount > 0) { - if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) { + if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { // No need to feedback when repeat delete key will have no effect. return; } @@ -3144,9 +2325,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if // it doesn't know what to do with it and leave it to the application. For example, // hardware key events for adjusting the screen's brightness are passed as is. - if (mEventInterpreter.onHardwareKeyEvent(event)) { + if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - mCurrentlyPressedHardwareKeys.add(keyIdentifier); + mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier); return true; } return super.onKeyDown(keyCode, event); @@ -3155,7 +2336,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public boolean onKeyUp(final int keyCode, final KeyEvent event) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { + if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { return true; } return super.onKeyUp(keyCode, event); @@ -3187,7 +2368,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { // Put the text in the attached EditText into a safe, saved state before switching to a // new activity that will also use the soft keyboard. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR); launchSubActivity(activityClass); } @@ -3253,26 +2434,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: can this be removed somehow without breaking the tests? @UsedForTesting - /* package for test */ String getFirstSuggestedWord() { - return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; + /* package for test */ SuggestedWords getSuggestedWords() { + // You may not use this method for anything else than debug + return DEBUG ? mInputLogic.mSuggestedWords : null; } // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. @UsedForTesting /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { - return mSuggest.isCurrentlyWaitingForMainDictionary(); - } - - // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. - @UsedForTesting - /* package for test */ boolean hasMainDictionary() { - return mSuggest.hasMainDictionary(); + return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary(); } // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. @UsedForTesting /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { - mSuggest.resetMainDict(this, locale, null); + mInputLogic.mSuggest.resetMainDict(this, locale, null); } public void debugDumpStateAndCrashWithException(final String context) { @@ -3297,7 +2473,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen p.println(" mIsSuggestionsRequested = " + settingsValues.isSuggestionsRequested(mDisplayOrientation)); p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); - p.println(" isComposingWord=" + mWordComposer.isComposingWord()); + p.println(" isComposingWord=" + mInputLogic.mWordComposer.isComposingWord()); p.println(" mSoundOn=" + settingsValues.mSoundOn); p.println(" mVibrateOn=" + settingsValues.mVibrateOn); p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java new file mode 100644 index 000000000..2f9221071 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -0,0 +1,1190 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.compat.SuggestionSpanUtils; +import com.android.inputmethod.event.EventInterpreter; +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LastComposedWord; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.RichInputConnection; +import com.android.inputmethod.latin.SubtypeSwitcher; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; +import com.android.inputmethod.latin.utils.RecapitalizeStatus; +import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.research.ResearchLogger; + +import java.util.TreeSet; + +/** + * This class manages the input logic. + */ +public final class InputLogic { + private static final String TAG = InputLogic.class.getSimpleName(); + + // TODO : Remove this member when we can. + private final LatinIME mLatinIME; + + // TODO : make all these fields private as soon as possible. + // Current space state of the input method. This can be any of the above constants. + public int mSpaceState; + // Never null + public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + public Suggest mSuggest; + // The event interpreter should never be null. + public EventInterpreter mEventInterpreter; + + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + public final WordComposer mWordComposer; + public final RichInputConnection mConnection; + public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); + + // Keep track of the last selection range to decide if we need to show word alternatives + public int mLastSelectionStart = Constants.NOT_A_CURSOR_POSITION; + public int mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION; + + public int mDeleteCount; + private long mLastKeyTime; + public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); + + // Keeps track of most recently inserted text (multi-character key) for reverting + public String mEnteredText; + + // TODO: This boolean is persistent state and causes large side effects at unexpected times. + // Find a way to remove it for readability. + public boolean mIsAutoCorrectionIndicatorOn; + + public InputLogic(final LatinIME latinIME) { + mLatinIME = latinIME; + mWordComposer = new WordComposer(); + mEventInterpreter = new EventInterpreter(latinIME); + mConnection = new RichInputConnection(latinIME); + } + + /** + * Initializes the input logic for input in an editor. + * + * Call this when input starts or restarts in some editor (typically, in onStartInputView). + * If the input is starting in the same field as before, set `restarting' to true. This allows + * the input logic to reset only necessary stuff and save performance. Also, when restarting + * some things must not be done (for example, the keyboard should not be reset to the + * alphabetic layout), so do not send false to this just in case. + * + * @param restarting whether input is starting in the same field as before. + */ + public void startInput(final boolean restarting) { + } + + /** + * Clean up the input logic after input is finished. + */ + public void finishInput() { + } + + /** + * React to a code input. It may be a code point to insert, or a symbolic value that influences + * the keyboard behavior. + * + * Typically, this is called whenever a key is pressed on the software keyboard. This is not + * the entry point for gesture input; see the onBatchInput* family of functions for this. + * + * @param code the code to handle. It may be a code point, or an internal key code. + * @param x the x-coordinate where the user pressed the key, or NOT_A_COORDINATE. + * @param y the y-coordinate where the user pressed the key, or NOT_A_COORDINATE. + */ + public void onCodeInput(final int code, final int x, final int y, + // TODO: remove these three arguments + final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher, + final SubtypeSwitcher subtypeSwitcher) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_onCodeInput(code, x, y); + } + final SettingsValues settingsValues = Settings.getInstance().getCurrent(); + final long when = SystemClock.uptimeMillis(); + if (code != Constants.CODE_DELETE + || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { + mDeleteCount = 0; + } + mLastKeyTime = when; + mConnection.beginBatchEdit(); + final KeyboardSwitcher switcher = keyboardSwitcher; + // The space state depends only on the last character pressed and its own previous + // state. Here, we revert the space state to neutral if the key is actually modifying + // the input contents (any non-shift key), which is what we should do for + // all inputs that do not result in a special state. Each character handling is then + // free to override the state as they see fit. + final int spaceState = mSpaceState; + if (!mWordComposer.isComposingWord()) { + mIsAutoCorrectionIndicatorOn = false; + } + + // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. + if (code != Constants.CODE_SPACE) { + handler.cancelDoubleSpacePeriodTimer(); + } + + boolean didAutoCorrect = false; + switch (code) { + case Constants.CODE_DELETE: + handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher); + LatinImeLogger.logOnDelete(x, y); + break; + case Constants.CODE_SHIFT: + // Note: Calling back to the keyboard on Shift key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + final Keyboard currentKeyboard = switcher.getKeyboard(); + if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout. + performRecapitalization(settingsValues, keyboardSwitcher); + } + break; + case Constants.CODE_CAPSLOCK: + // Note: Changing keyboard to shift lock state is handled in + // {@link KeyboardSwitcher#onCodeInput(int)}. + break; + case Constants.CODE_SWITCH_ALPHA_SYMBOL: + // Note: Calling back to the keyboard on symbol key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SETTINGS: + onSettingsKeyPressed(); + break; + case Constants.CODE_SHORTCUT: + subtypeSwitcher.switchToShortcutIME(mLatinIME); + break; + case Constants.CODE_ACTION_NEXT: + performEditorAction(EditorInfo.IME_ACTION_NEXT); + break; + case Constants.CODE_ACTION_PREVIOUS: + performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); + break; + case Constants.CODE_LANGUAGE_SWITCH: + handleLanguageSwitchKey(); + break; + case Constants.CODE_EMOJI: + // Note: Switching emoji keyboard is being handled in + // {@link KeyboardState#onCodeInput(int,int)}. + break; + case Constants.CODE_ENTER: + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final int imeOptionsActionId = + InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); + if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { + // Either we have an actionLabel and we should performEditorAction with actionId + // regardless of its value. + performEditorAction(editorInfo.actionId); + } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { + // We didn't have an actionLabel, but we had another action to execute. + // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, + // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it + // means there should be an action and the app didn't bother to set a specific + // code for it - presumably it only handles one. It does not have to be treated + // in any specific way: anything that is not IME_ACTION_NONE should be sent to + // performEditorAction. + performEditorAction(imeOptionsActionId); + } else { + // No action label, and the action from imeOptions is NONE: this is a regular + // enter key that should input a carriage return. + didAutoCorrect = handleNonSpecialCharacter(settingsValues, + Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler); + } + break; + case Constants.CODE_SHIFT_ENTER: + didAutoCorrect = handleNonSpecialCharacter(settingsValues, + Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler); + break; + default: + didAutoCorrect = handleNonSpecialCharacter(settingsValues, + code, x, y, spaceState, keyboardSwitcher, handler); + break; + } + switcher.onCodeInput(code); + // Reset after any single keystroke, except shift, capslock, and symbol-shift + if (!didAutoCorrect && code != Constants.CODE_SHIFT + && code != Constants.CODE_CAPSLOCK + && code != Constants.CODE_SWITCH_ALPHA_SYMBOL) + mLastComposedWord.deactivate(); + if (Constants.CODE_DELETE != code) { + mEnteredText = null; + } + mConnection.endBatchEdit(); + } + + /** + * Handle inputting a code point to the editor. + * + * Non-special keys are those that generate a single code point. + * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that + * manage keyboard-related stuff like shift, language switch, settings, layout switch, or + * any key that results in multiple code points like the ".com" key. + * + * @param settingsValues The current settings values. + * @param codePoint the code point associated with the key. + * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param spaceState the space state at start of the batch input. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleNonSpecialCharacter(final SettingsValues settingsValues, + final int codePoint, final int x, final int y, final int spaceState, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + mSpaceState = SpaceState.NONE; + final boolean didAutoCorrect; + if (settingsValues.isWordSeparator(codePoint) + || Character.getType(codePoint) == Character.OTHER_SYMBOL) { + didAutoCorrect = handleSeparator(settingsValues, codePoint, x, y, spaceState, + keyboardSwitcher, handler); + } else { + didAutoCorrect = false; + if (SpaceState.PHANTOM == spaceState) { + if (settingsValues.mIsInternal) { + if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { + LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ", + mWordComposer); + } + } + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd); + } else { + commitTyped(LastComposedWord.NOT_A_SEPARATOR); + } + } + final int keyX, keyY; + final Keyboard keyboard = keyboardSwitcher.getKeyboard(); + if (keyboard != null && keyboard.hasProximityCharsCorrection(codePoint)) { + keyX = x; + keyY = y; + } else { + keyX = Constants.NOT_A_COORDINATE; + keyY = Constants.NOT_A_COORDINATE; + } + handleNonSeparator(settingsValues, codePoint, keyX, keyY, spaceState, + keyboardSwitcher, handler); + } + return didAutoCorrect; + } + + /** + * Handle a non-separator. + * @param settingsValues The current settings values. + * @param codePoint the code point associated with the key. + * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param spaceState the space state at start of the batch input. + */ + private void handleNonSeparator(final SettingsValues settingsValues, + final int codePoint, final int x, final int y, final int spaceState, + // TODO: Remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + // TODO: refactor this method to stop flipping isComposingWord around all the time, and + // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter + // which has the same name as other handle* methods but is not the same. + boolean isComposingWord = mWordComposer.isComposingWord(); + + // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. + // See onStartBatchInput() to see how to do it. + if (SpaceState.PHANTOM == spaceState && !settingsValues.isWordConnector(codePoint)) { + if (isComposingWord) { + // Sanity check + throw new RuntimeException("Should not be composing here"); + } + promotePhantomSpace(settingsValues); + } + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd); + isComposingWord = false; + } + // We want to find out whether to start composing a new word with this character. If so, + // we need to reset the composing state and switch isComposingWord. The order of the + // tests is important for good performance. + // We only start composing if we're not already composing. + if (!isComposingWord + // We only start composing if this is a word code point. Essentially that means it's a + // a letter or a word connector. + && settingsValues.isWordCodePoint(codePoint) + // We never go into composing state if suggestions are not requested. + && settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation) && + // In languages with spaces, we only start composing a word when we are not already + // touching a word. In languages without spaces, the above conditions are sufficient. + (!mConnection.isCursorTouchingWord(settingsValues) + || !settingsValues.mCurrentLanguageHasSpaces)) { + // Reset entirely the composing state anyway, then start composing a new word unless + // the character is a single quote or a dash. The idea here is, single quote and dash + // are not separators and they should be treated as normal characters, except in the + // first position where they should not start composing a word. + isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint + && Constants.CODE_DASH != codePoint); + // Here we don't need to reset the last composed word. It will be reset + // when we commit this one, if we ever do; if on the other hand we backspace + // it entirely and resume suggestions on the previous word, we'd like to still + // have touch coordinates for it. + resetComposingState(false /* alsoResetLastComposedWord */); + } + if (isComposingWord) { + final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView(); + // TODO: We should reconsider which coordinate system should be used to represent + // keyboard event. + final int keyX = mainKeyboardView.getKeyX(x); + final int keyY = mainKeyboardView.getKeyY(y); + mWordComposer.add(codePoint, keyX, keyY); + // If it's the first letter, make note of auto-caps state + if (mWordComposer.size() == 1) { + // We pass 1 to getPreviousWordForSuggestion because we were not composing a word + // yet, so the word we want is the 1st word before the cursor. + mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + getActualCapsMode(keyboardSwitcher), + getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */)); + } + mConnection.setComposingText(getTextWithUnderline( + mWordComposer.getTypedWord()), 1); + } else { + final boolean swapWeakSpace = maybeStripSpace(settingsValues, + codePoint, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); + + sendKeyCodePoint(codePoint); + + if (swapWeakSpace) { + swapSwapperAndSpace(keyboardSwitcher); + mSpaceState = SpaceState.WEAK; + } + // In case the "add to dictionary" hint was still displayed. + mLatinIME.dismissAddToDictionaryHint(); + } + handler.postUpdateSuggestionStrip(); + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onNonSeparator((char)codePoint, x, y); + } + } + + /** + * Handle input of a separator code point. + * @param settingsValues The current settings values. + * @param codePoint the code point associated with the key. + * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable. + * @param spaceState the space state at start of the batch input. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleSeparator(final SettingsValues settingsValues, + final int codePoint, final int x, final int y, final int spaceState, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + boolean didAutoCorrect = false; + // We avoid sending spaces in languages without spaces if we were composing. + final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint + && !settingsValues.mCurrentLanguageHasSpaces + && mWordComposer.isComposingWord(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the separator at the current cursor position. + resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd); + } + // isComposingWord() may have changed since we stored wasComposing + if (mWordComposer.isComposingWord()) { + if (settingsValues.mCorrectionEnabled) { + final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR + : StringUtils.newSingleCodePointString(codePoint); + commitCurrentAutoCorrection(settingsValues, separator, handler); + didAutoCorrect = true; + } else { + commitTyped(StringUtils.newSingleCodePointString(codePoint)); + } + } + + final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState, + Constants.SUGGESTION_STRIP_COORDINATE == x); + + if (SpaceState.PHANTOM == spaceState && + settingsValues.isUsuallyPrecededBySpace(codePoint)) { + promotePhantomSpace(settingsValues); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord()); + } + + if (!shouldAvoidSendingCode) { + sendKeyCodePoint(codePoint); + } + + if (Constants.CODE_SPACE == codePoint) { + if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation)) { + if (maybeDoubleSpacePeriod(settingsValues, keyboardSwitcher, handler)) { + mSpaceState = SpaceState.DOUBLE; + } else if (!mLatinIME.isShowingPunctuationList()) { + mSpaceState = SpaceState.WEAK; + } + } + + handler.startDoubleSpacePeriodTimer(); + handler.postUpdateSuggestionStrip(); + } else { + if (swapWeakSpace) { + swapSwapperAndSpace(keyboardSwitcher); + mSpaceState = SpaceState.SWAP_PUNCTUATION; + } else if (SpaceState.PHANTOM == spaceState + && settingsValues.isUsuallyFollowedBySpace(codePoint)) { + // If we are in phantom space state, and the user presses a separator, we want to + // stay in phantom space state so that the next keypress has a chance to add the + // space. For example, if I type "Good dat", pick "day" from the suggestion strip + // then insert a comma and go on to typing the next word, I want the space to be + // inserted automatically before the next word, the same way it is when I don't + // input the comma. + // The case is a little different if the separator is a space stripper. Such a + // separator does not normally need a space on the right (that's the difference + // between swappers and strippers), so we should not stay in phantom space state if + // the separator is a stripper. Hence the additional test above. + mSpaceState = SpaceState.PHANTOM; + } + + // Set punctuation right away. onUpdateSelection will fire but tests whether it is + // already displayed or not, so it's okay. + mLatinIME.setPunctuationSuggestions(); + } + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator((char)codePoint, x, y); + } + + keyboardSwitcher.updateShiftState(); + return didAutoCorrect; + } + + /** + * Handle a press on the backspace key. + * @param settingsValues The current settings values. + * @param spaceState The space state at start of this batch edit. + */ + private void handleBackspace(final SettingsValues settingsValues, final int spaceState, + // TODO: remove these arguments + final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) { + mSpaceState = SpaceState.NONE; + mDeleteCount++; + + // In many cases, we may have to put the keyboard in auto-shift state again. However + // we want to wait a few milliseconds before doing it to avoid the keyboard flashing + // during key repeat. + handler.postUpdateShiftState(); + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can remove the character at the current cursor position. + resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd); + // When we exit this if-clause, mWordComposer.isComposingWord() will return false. + } + if (mWordComposer.isComposingWord()) { + if (mWordComposer.isBatchMode()) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + final String word = mWordComposer.getTypedWord(); + ResearchLogger.latinIME_handleBackspace_batch(word, 1); + } + final String rejectedSuggestion = mWordComposer.getTypedWord(); + mWordComposer.reset(); + mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); + } else { + mWordComposer.deleteLast(); + } + mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + handler.postUpdateSuggestionStrip(); + if (!mWordComposer.isComposingWord()) { + // If we just removed the last character, auto-caps mode may have changed so we + // need to re-evaluate. + keyboardSwitcher.updateShiftState(); + } + } else { + if (mLastComposedWord.canRevertCommit()) { + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onAutoCorrectionCancellation(); + } + revertCommit(settingsValues, keyboardSwitcher, handler); + return; + } + if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { + // Cancel multi-character input: remove the text we just entered. + // This is triggered on backspace after a key that inputs multiple characters, + // like the smiley key or the .com key. + mConnection.deleteSurroundingText(mEnteredText.length(), 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); + } + mEnteredText = null; + // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. + // In addition we know that spaceState is false, and that we should not be + // reverting any autocorrect at this point. So we can safely return. + return; + } + if (SpaceState.DOUBLE == spaceState) { + handler.cancelDoubleSpacePeriodTimer(); + if (mConnection.revertDoubleSpacePeriod()) { + // No need to reset mSpaceState, it has already be done (that's why we + // receive it as a parameter) + return; + } + } else if (SpaceState.SWAP_PUNCTUATION == spaceState) { + if (mConnection.revertSwapPunctuation()) { + // Likewise + return; + } + } + + // No cancelling of commit/double space/swap: we have a regular backspace. + // We should backspace one char and restart suggestion if at the end of a word. + if (mLastSelectionStart != mLastSelectionEnd) { + // If there is a selection, remove it. + final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; + mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); + // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to + // happen, and if it's wrong, the next call to onUpdateSelection will correct it, + // but we want to set it right away to avoid it being used with the wrong values + // later (typically, in a subsequent press on backspace). + mLastSelectionEnd = mLastSelectionStart; + mConnection.deleteSurroundingText(numCharsDeleted, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(numCharsDeleted, + false /* shouldUncommitLogUnit */); + } + } else { + // There is no selection, just delete one character. + if (Constants.NOT_A_CURSOR_POSITION == mLastSelectionEnd) { + // This should never happen. + Log.e(TAG, "Backspace when we don't know the selection position"); + } + if (mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean() || + settingsValues.mInputAttributes.isTypeNull()) { + // There are two possible reasons to send a key event: either the field has + // type TYPE_NULL, in which case the keyboard should send events, or we are + // running in backward compatibility mode. Before Jelly bean, the keyboard + // would simulate a hardware keyboard event on pressing enter or delete. This + // is bad for many reasons (there are race conditions with commits) but some + // applications are relying on this behavior so we continue to support it for + // older apps, so we retain this behavior if the app has target SDK < JellyBean. + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + } + } else { + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursor == Constants.NOT_A_CODE) { + // Nothing to delete before the cursor. + return; + } + final int lengthToDelete = + Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDelete, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(lengthToDelete, + true /* shouldUncommitLogUnit */); + } + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + final int codePointBeforeCursorToDeleteAgain = + mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { + final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( + codePointBeforeCursorToDeleteAgain) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain, + true /* shouldUncommitLogUnit */); + } + } + } + } + } + // TODO: move mDisplayOrientation to CurrentSettings. + if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation) + && settingsValues.mCurrentLanguageHasSpaces) { + restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(settingsValues, keyboardSwitcher, + handler); + } + // We just removed a character. We need to update the auto-caps state. + keyboardSwitcher.updateShiftState(); + } + } + + /** + * Handle a press on the language switch key (the "globe key") + */ + private void handleLanguageSwitchKey() { + mLatinIME.handleLanguageSwitchKey(); + } + + /** + * Swap a space with a space-swapping punctuation sign. + * + * This method will check that there are two characters before the cursor and that the first + * one is a space before it does the actual swapping. + */ + // TODO: Remove this argument + private void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) { + final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); + // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. + if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) { + mConnection.deleteSurroundingText(2, 0); + final String text = lastTwo.charAt(1) + " "; + mConnection.commitText(text, 1); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); + } + keyboardSwitcher.updateShiftState(); + } + } + + /* + * Strip a trailing space if necessary and returns whether it's a swap weak space situation. + * @param settingsValues The current settings values. + * @param codePoint The code point that is about to be inserted. + * @param spaceState The space state at start of this batch edit. + * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip. + * @return whether we should swap the space instead of removing it. + */ + private boolean maybeStripSpace(final SettingsValues settingsValues, + final int code, final int spaceState, final boolean isFromSuggestionStrip) { + if (Constants.CODE_ENTER == code && SpaceState.SWAP_PUNCTUATION == spaceState) { + mConnection.removeTrailingSpace(); + return false; + } + if ((SpaceState.WEAK == spaceState || SpaceState.SWAP_PUNCTUATION == spaceState) + && isFromSuggestionStrip) { + if (settingsValues.isUsuallyPrecededBySpace(code)) return false; + if (settingsValues.isUsuallyFollowedBySpace(code)) return true; + mConnection.removeTrailingSpace(); + } + return false; + } + + /** + * Apply the double-space-to-period transformation if applicable. + * + * The double-space-to-period transformation means that we replace two spaces with a + * period-space sequence of characters. This typically happens when the user presses space + * twice in a row quickly. + * This method will check that the double-space-to-period is active in settings, that the + * two spaces have been input close enough together, and that the previous character allows + * for the transformation to take place. If all of these conditions are fulfilled, this + * method applies the transformation and returns true. Otherwise, it does nothing and + * returns false. + * + * @param settingsValues the current values of the settings. + * @return true if we applied the double-space-to-period transformation, false otherwise. + */ + private boolean maybeDoubleSpacePeriod(final SettingsValues settingsValues, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + if (!settingsValues.mUseDoubleSpacePeriod) return false; + if (!handler.isAcceptingDoubleSpacePeriod()) return false; + // We only do this when we see two spaces and an accepted code point before the cursor. + // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. + final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); + if (null == lastThree) return false; + final int length = lastThree.length(); + if (length < 3) return false; + if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; + if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; + // We know there are spaces in pos -1 and -2, and we have at least three chars. + // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, + // so this is fine. + final int firstCodePoint = + Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? + Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); + if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { + handler.cancelDoubleSpacePeriodTimer(); + mConnection.deleteSurroundingText(2, 0); + final String textToInsert = new String( + new int[] { settingsValues.mSentenceSeparator, Constants.CODE_SPACE }, 0, 2); + mConnection.commitText(textToInsert, 1); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, + false /* isBatchMode */); + } + mWordComposer.discardPreviousWordForSuggestion(); + keyboardSwitcher.updateShiftState(); + return true; + } + return false; + } + + /** + * Returns whether this code point can be followed by the double-space-to-period transformation. + * + * See #maybeDoubleSpaceToPeriod for details. + * Generally, most word characters can be followed by the double-space-to-period transformation, + * while most punctuation can't. Some punctuation however does allow for this to take place + * after them, like the closing parenthesis for example. + * + * @param codePoint the code point after which we may want to apply the transformation + * @return whether it's fine to apply the transformation after this code point. + */ + private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { + // TODO: This should probably be a blacklist rather than a whitelist. + // TODO: This should probably be language-dependant... + return Character.isLetterOrDigit(codePoint) + || codePoint == Constants.CODE_SINGLE_QUOTE + || codePoint == Constants.CODE_DOUBLE_QUOTE + || codePoint == Constants.CODE_CLOSING_PARENTHESIS + || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET + || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET + || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET + || codePoint == Constants.CODE_PLUS + || codePoint == Constants.CODE_PERCENT + || Character.getType(codePoint) == Character.OTHER_SYMBOL; + } + + /** + * Performs a recapitalization event. + * @param settingsValues The current settings values. + */ + private void performRecapitalization(final SettingsValues settingsValues, + // TODO: remove this argument. + final KeyboardSwitcher keyboardSwitcher) { + if (mLastSelectionStart == mLastSelectionEnd) { + return; // No selection + } + // If we have a recapitalize in progress, use it; otherwise, create a new one. + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + final CharSequence selectedText = + mConnection.getSelectedText(0 /* flags, 0 for no styles */); + if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection + mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, + selectedText.toString(), + settingsValues.mLocale, settingsValues.mWordSeparators); + // We trim leading and trailing whitespace. + mRecapitalizeStatus.trim(); + // Trimming the object may have changed the length of the string, and we need to + // reposition the selection handles accordingly. As this result in an IPC call, + // only do it if it's actually necessary, in other words if the recapitalize status + // is not set at the same place as before. + if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + } + } + mConnection.finishComposingText(); + mRecapitalizeStatus.rotate(); + final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; + mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); + // Match the keyboard to the new state. + keyboardSwitcher.updateShiftState(); + } + + /** + * Check if the cursor is actually at the end of a word. If so, restart suggestions on this + * word, otherwise do nothing. + * @param settingsValues the current values of the settings. + */ + private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord( + final SettingsValues settingsValues, + // TODO: remove these two arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(settingsValues); + if (null != word) { + final String wordString = word.toString(); + mWordComposer.setComposingWord(word, + // Previous word is the 2nd word before cursor because we are restarting on the + // 1st word before cursor. + getNthPreviousWordForSuggestion(settingsValues, 2 /* nthPreviousWord */), + keyboardSwitcher.getKeyboard()); + final int length = word.length(); + mConnection.deleteSurroundingText(length, 0); + mConnection.setComposingText(word, 1); + handler.postUpdateSuggestionStrip(); + // TODO: Handle the case where the user manually moves the cursor and then backs up over + // a separator. In that case, the current log unit should not be uncommitted. + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, + true /* dumpCurrentLogUnit */); + } + } + } + + /** + * Reverts a previous commit with auto-correction. + * + * This is triggered upon pressing backspace just after a commit with auto-correction. + * + * @param settingsValues the current settings values. + */ + private void revertCommit(final SettingsValues settingsValues, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + final String previousWord = mLastComposedWord.mPrevWord; + final String originallyTypedWord = mLastComposedWord.mTypedWord; + final String committedWord = mLastComposedWord.mCommittedWord; + final int cancelLength = committedWord.length(); + // We want java chars, not codepoints for the following. + final int separatorLength = mLastComposedWord.mSeparatorString.length(); + // TODO: should we check our saved separator against the actual contents of the text view? + final int deleteLength = cancelLength + separatorLength; + if (LatinIME.DEBUG) { + if (mWordComposer.isComposingWord()) { + throw new RuntimeException("revertCommit, but we are composing a word"); + } + final CharSequence wordBeforeCursor = + mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); + if (!TextUtils.equals(committedWord, wordBeforeCursor)) { + throw new RuntimeException("revertCommit check failed: we thought we were " + + "reverting \"" + committedWord + + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); + } + } + mConnection.deleteSurroundingText(deleteLength, 0); + if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { + if (mSuggest != null) { + mSuggest.cancelAddingUserHistory(previousWord, committedWord); + } + } + final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + if (settingsValues.mCurrentLanguageHasSpaces) { + // For languages with spaces, we revert to the typed string, but the cursor is still + // after the separator so we don't resume suggestions. If the user wants to correct + // the word, they have to press backspace again. + mConnection.commitText(stringToCommit, 1); + } else { + // For languages without spaces, we revert the typed string but the cursor is flush + // with the typed word, so we need to resume suggestions right away. + mWordComposer.setComposingWord(stringToCommit, previousWord, + keyboardSwitcher.getKeyboard()); + mConnection.setComposingText(stringToCommit, 1); + } + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, + mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); + } + // Don't restart suggestion yet. We'll restart if the user deletes the + // separator. + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + // We have a separator between the word and the cursor: we should show predictions. + handler.postUpdateSuggestionStrip(); + } + + /** + * Factor in auto-caps and manual caps and compute the current caps mode. + * @param keyboardSwitcher the keyboard switcher. Caps mode depends on its mode. + * @return the actual caps mode the keyboard is in right now. + */ + // TODO: Make this private + public int getActualCapsMode(final KeyboardSwitcher keyboardSwitcher) { + final int keyboardShiftMode = keyboardSwitcher.getKeyboardShiftMode(); + if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; + final int auto = mLatinIME.getCurrentAutoCapsState(); + if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { + return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; + } + if (0 != auto) { + return WordComposer.CAPS_MODE_AUTO_SHIFTED; + } + return WordComposer.CAPS_MODE_OFF; + } + + /** + * @return the editor info for the current editor + */ + private EditorInfo getCurrentInputEditorInfo() { + return mLatinIME.getCurrentInputEditorInfo(); + } + + /** + * Get the nth previous word before the cursor as context for the suggestion process. + * @param currentSettings the current settings values. + * @param nthPreviousWord reverse index of the word to get (1-indexed) + * @return the nth previous word before the cursor. + */ + // TODO: Make this private + public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings, + final int nthPreviousWord) { + if (currentSettings.mCurrentLanguageHasSpaces) { + // If we are typing in a language with spaces we can just look up the previous + // word from textview. + return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord); + } else { + return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null + : mLastComposedWord.mCommittedWord; + } + } + + /** + * @param actionId the action to perform + */ + private void performEditorAction(final int actionId) { + mConnection.performEditorAction(actionId); + } + + /** + * Handle a press on the settings key. + */ + private void onSettingsKeyPressed() { + mLatinIME.onSettingsKeyPressed(); + } + + /** + * Resets the whole input state to the starting state. + * + * This will clear the composing word, reset the last composed word, clear the suggestion + * strip and tell the input connection about it so that it can refresh its caches. + * + * @param settingsValues the current values of the settings. + * @param newSelStart the new selection start, in java characters. + * @param newSelEnd the new selection end, in java characters. + */ + // TODO: how is this different from startInput ?! + // TODO: remove all references to this in LatinIME and make this private + public void resetEntireInputState(final SettingsValues settingsValues, + final int newSelStart, final int newSelEnd) { + final boolean shouldFinishComposition = mWordComposer.isComposingWord(); + resetComposingState(true /* alsoResetLastComposedWord */); + if (settingsValues.mBigramPredictionEnabled) { + mLatinIME.clearSuggestionStrip(); + } else { + mLatinIME.setSuggestedWords(settingsValues.mSuggestPuncList, false); + } + mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, + shouldFinishComposition); + } + + /** + * Resets only the composing state. + * + * Compare #resetEntireInputState, which also clears the suggestion strip and resets the + * input connection caches. This only deals with the composing state. + * + * @param alsoResetLastComposedWord whether to also reset the last composed word. + */ + // TODO: remove all references to this in LatinIME and make this private. + public void resetComposingState(final boolean alsoResetLastComposedWord) { + mWordComposer.reset(); + if (alsoResetLastComposedWord) { + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + } + } + + /** + * Gets a chunk of text with or the auto-correction indicator underline span as appropriate. + * + * This method looks at the old state of the auto-correction indicator to put or not put + * the underline span as appropriate. It is important to note that this does not correspond + * exactly to whether this word will be auto-corrected to or not: what's important here is + * to keep the same indication as before. + * When we add a new code point to a composing word, we don't know yet if we are going to + * auto-correct it until the suggestions are computed. But in the mean time, we still need + * to display the character and to extend the previous underline. To avoid any flickering, + * the underline should keep the same color it used to have, even if that's not ultimately + * the correct color for this new word. When the suggestions are finished evaluating, we + * will call this method again to fix the color of the underline. + * + * @param text the text on which to maybe apply the span. + * @return the same text, with the auto-correction underline span if that's appropriate. + */ + // TODO: remove all references to this in LatinIME and make this private. Also, shouldn't + // this go in some *Utils class instead? + public CharSequence getTextWithUnderline(final String text) { + return mIsAutoCorrectionIndicatorOn + ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text) + : text; + } + + /** + * Sends a DOWN key event followed by an UP key event to the editor. + * + * If possible at all, avoid using this method. It causes all sorts of race conditions with + * the text view because it goes through a different, asynchronous binder. Also, batch edits + * are ignored for key events. Use the normal software input methods instead. + * + * @param keyCode the key code to send inside the key event. + */ + private void sendDownUpKeyEvent(final int keyCode) { + final long eventTime = SystemClock.uptimeMillis(); + mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + + /** + * Sends a code point to the editor, using the most appropriate method. + * + * Normally we send code points with commitText, but there are some cases (where backward + * compatibility is a concern for example) where we want to use deprecated methods. + * + * @param codePoint the code point to send. + */ + private void sendKeyCodePoint(final int codePoint) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_sendKeyCodePoint(codePoint); + } + // TODO: Remove this special handling of digit letters. + // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. + if (codePoint >= '0' && codePoint <= '9') { + sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); + return; + } + + // TODO: we should do this also when the editor has TYPE_NULL + if (Constants.CODE_ENTER == codePoint + && mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean()) { + // Backward compatibility mode. Before Jelly bean, the keyboard would simulate + // a hardware keyboard event on pressing enter or delete. This is bad for many + // reasons (there are race conditions with commits) but some applications are + // relying on this behavior so we continue to support it for older apps. + sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); + } else { + mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); + } + } + + /** + * Promote a phantom space to an actual space. + * + * This essentially inserts a space, and that's it. It just checks the options and the text + * before the cursor are appropriate before doing it. + * + * @param settingsValues the current values of the settings. + */ + // TODO: Make this private. + public void promotePhantomSpace(final SettingsValues settingsValues) { + if (settingsValues.shouldInsertSpacesAutomatically() + && settingsValues.mCurrentLanguageHasSpaces + && !mConnection.textBeforeCursorLooksLikeURL()) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_promotePhantomSpace(); + } + sendKeyCodePoint(Constants.CODE_SPACE); + } + } + + /** + * Commit the typed string to the editor. + * + * This is typically called when we should commit the currently composing word without applying + * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard + * is configured to not do auto-correction at all (because of the settings or the properties of + * the editor). In this case, `separatorString' is set to the separator that was pressed. + * We also come here in a variety of cases with external user action. For example, when the + * cursor is moved while there is a composition, or when the keyboard is closed, or when the + * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected. + * In this case, `separatorString' is set to NOT_A_SEPARATOR. + * + * @param separatorString the separator string that's causing the commit, or NOT_A_SEPARATOR if + * none. + */ + // TODO: Make this private + public void commitTyped(final String separatorString) { + if (!mWordComposer.isComposingWord()) return; + final String typedWord = mWordComposer.getTypedWord(); + if (typedWord.length() > 0) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); + } + mLatinIME.commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, + separatorString); + } + } + + /** + * Commit the current auto-correction. + * + * This will commit the best guess of the keyboard regarding what the user meant by typing + * the currently composing word. The IME computes suggestions and assigns a confidence score + * to each of them; when it's confident enough in one suggestion, it replaces the typed string + * by this suggestion at commit time. When it's not confident enough, or when it has no + * suggestions, or when the settings or environment does not allow for auto-correction, then + * this method just commits the typed string. + * Note that if suggestions are currently being computed in the background, this method will + * block until the computation returns. This is necessary for consistency (it would be very + * strange if pressing space would commit a different word depending on how fast you press). + * + * @param settingsValues the current value of the settings. + * @param separator the separator that's causing the commit to happen. + */ + // TODO: Make this private + public void commitCurrentAutoCorrection(final SettingsValues settingsValues, + final String separator, + // TODO: Remove this argument. + final LatinIME.UIHandler handler) { + // Complete any pending suggestions query first + if (handler.hasPendingUpdateSuggestions()) { + mLatinIME.updateSuggestionStrip(); + } + final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final String typedWord = mWordComposer.getTypedWord(); + final String autoCorrection = (typedAutoCorrection != null) + ? typedAutoCorrection : typedWord; + if (autoCorrection != null) { + if (TextUtils.isEmpty(typedWord)) { + throw new RuntimeException("We have an auto-correction but the typed word " + + "is empty? Impossible! I must commit suicide."); + } + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onAutoCorrection( + typedWord, autoCorrection, separator, mWordComposer); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + final SuggestedWords suggestedWords = mSuggestedWords; + ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, + separator, mWordComposer.isBatchMode(), suggestedWords); + } + mLatinIME.commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, + separator); + if (!typedWord.equals(autoCorrection)) { + // This will make the correction flash for a short while as a visual clue + // to the user that auto-correction happened. It has no other effect; in particular + // note that this won't affect the text inside the text field AT ALL: it only makes + // the segment of text starting at the supplied index and running for the length + // of the auto-correction flash. At this moment, the "typedWord" argument is + // ignored by TextView. + mConnection.commitCorrection( + new CorrectionInfo(mLastSelectionEnd - typedWord.length(), + typedWord, autoCorrection)); + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java new file mode 100644 index 000000000..ce80c0016 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +/** + * Class for managing space states. + * + * At any given time, the input logic is in one of five possible space states. Depending on the + * current space state, some behavior will change; the prime example of this is the PHANTOM state, + * in which any subsequent letter input will input a space before the letter. Read on the + * description inside this class for each of the space states. + */ +public class SpaceState { + // None: the state where all the keyboard behavior is the most "standard" and no automatic + // input is added or removed. In this state, all self-inserting keys only insert themselves, + // and backspace removes one character. + public static final int NONE = 0; + // Double space: the state where the user pressed space twice quickly, which LatinIME + // resolved as period-space. In this state, pressing backspace will undo the + // double-space-to-period insertion: it will replace ". " with " ". + public static final int DOUBLE = 1; + // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip + // have just been swapped. In this state, pressing backspace will undo the swap: the + // characters will be swapped back back, and the space state will go to WEAK. + public static final int SWAP_PUNCTUATION = 2; + // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak + // spaces happen when the user presses space, accepting the current suggestion (whether + // it's an auto-correction or not). In this state, pressing a punctuation from the suggestion + // strip inserts it before the space (while it inserts it after the space in the NONE state). + public static final int WEAK = 3; + // Phantom space: a not-yet-inserted space that should get inserted on the next input, + // character provided it's not a separator. If it's a separator, the phantom space is dropped. + // Phantom spaces happen when a user chooses a word from the suggestion strip. In this state, + // non-separators insert a space before they get inserted. + public static final int PHANTOM = 4; + + private SpaceState() { + // This class is not publicly instantiable. + } +} |