diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/inputlogic')
3 files changed, 386 insertions, 404 deletions
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index fdab7f25f..5b3b28d75 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin.inputlogic; import android.graphics.Color; -import android.inputmethodservice.InputMethodService; import android.os.SystemClock; import android.text.SpannableString; import android.text.Spanned; @@ -28,32 +27,28 @@ import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.inputmethod.CorrectionInfo; -import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.InputTransaction; +import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardSwitcher; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.keyboard.TextDecorator; -import com.android.inputmethod.keyboard.TextDecoratorUiOperator; -import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; -import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LastComposedWord; import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.RichInputConnection; import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.common.Constants; +import com.android.inputmethod.latin.common.InputPointers; +import com.android.inputmethod.latin.common.StringUtils; import com.android.inputmethod.latin.define.DebugFlags; -import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -61,13 +56,15 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.RecapitalizeStatus; -import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.TextRange; import java.util.ArrayList; import java.util.TreeSet; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + /** * This class manages the input logic. */ @@ -75,7 +72,7 @@ public final class InputLogic { private static final String TAG = InputLogic.class.getSimpleName(); // TODO : Remove this member when we can. - private final LatinIME mLatinIME; + final LatinIME mLatinIME; private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; // Never null. @@ -85,18 +82,10 @@ public final class InputLogic { // Current space state of the input method. This can be any of the above constants. private int mSpaceState; // Never null - public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); public final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; - private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() { - @Override - public void onClickComposingTextToAddToDictionary(final String word) { - mLatinIME.addWordToUserDictionary(word); - mLatinIME.dismissAddToDictionaryHint(); - } - }); - public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; // This has package visibility so it can be accessed from InputLogicHandler. /* package */ final WordComposer mWordComposer; @@ -144,13 +133,20 @@ public final class InputLogic { */ public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; + if (!mWordComposer.getTypedWord().isEmpty()) { + // For messaging apps that offer send button, the IME does not get the opportunity + // to capture the last word. This block should capture those uncommitted words. + // The timestamp at which it is captured is not accurate but close enough. + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); + } mWordComposer.restartCombining(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SpaceState.NONE; mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once mCurrentlyPressedHardwareKeys.clear(); - mSuggestedWords = SuggestedWords.EMPTY; + mSuggestedWords = SuggestedWords.getEmptyInstance(); // 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. mConnection.tryFixLyingCursorPosition(); @@ -161,13 +157,9 @@ public final class InputLogic { mInputLogicHandler.reset(); } - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - // AcceptTypedWord feature relies on CursorAnchorInfo. - if (settingsValues.mShouldShowUiToAcceptTypedWord) { - mConnection.requestCursorUpdates(true /* enableMonitor */, - true /* requestImmediateCallback */); - } - mTextDecorator.reset(); + if (settingsValues.mShouldShowLxxSuggestionUi) { + mConnection.requestCursorUpdates(true /* enableMonitor */, + true /* requestImmediateCallback */); } } @@ -204,6 +196,8 @@ public final class InputLogic { public void finishInput() { if (mWordComposer.isComposingWord()) { mConnection.finishComposingText(); + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); } resetComposingState(true /* alsoResetLastComposedWord */); mInputLogicHandler.reset(); @@ -231,9 +225,7 @@ public final class InputLogic { * @return the complete transaction object */ public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event, - final int keyboardShiftMode, - // TODO: remove this argument - final LatinIME.UIHandler handler) { + final int keyboardShiftMode, final LatinIME.UIHandler handler) { final String rawText = event.getTextToCommit().toString(); final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, SystemClock.uptimeMillis(), mSpaceState, @@ -247,9 +239,10 @@ public final class InputLogic { handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING); final String text = performSpecificTldProcessingOnTextInput(rawText); if (SpaceState.PHANTOM == mSpaceState) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } mConnection.commitText(text, 1); + StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.NONE; @@ -260,20 +253,6 @@ public final class InputLogic { } /** - * Determines whether "Touch again to save" should be shown or not. - * @param suggestionInfo the suggested word chosen by the user. - * @return {@code true} if we should show the "Touch again to save" hint. - */ - private boolean shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo) { - // 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). - return (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) - || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION)) - && !mDictionaryFacilitator.isValidWord(suggestionInfo.mWord, true /* ignoreCase */) - && mDictionaryFacilitator.isUserDictionaryEnabled(); - } - - /** * A suggestion was picked from the suggestion strip. * @param settingsValues the current values of the settings. * @param suggestionInfo the suggestion info. @@ -285,12 +264,14 @@ public final class InputLogic { // interface public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, - // TODO: remove these arguments final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { final SuggestedWords suggestedWords = mSuggestedWords; final String suggestion = suggestionInfo.mWord; // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { + // We still want to log a suggestion click. + StatsUtils.onPickSuggestionManually( + mSuggestedWords, suggestionInfo, mDictionaryFacilitator); // Word separators are suggested before the user inputs something. // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); @@ -312,7 +293,7 @@ public final class InputLogic { final int firstChar = Character.codePointAt(suggestion, 0); if (!settingsValues.isWordSeparator(firstChar) || settingsValues.isUsuallyPrecededBySpace(firstChar)) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } } @@ -321,7 +302,7 @@ public final class InputLogic { // however need to reset the suggestion strip right away, because we know we can't take // the risk of calling commitCompletion twice because we don't know how the app will react. if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) { - mSuggestedWords = SuggestedWords.EMPTY; + mSuggestedWords = SuggestedWords.getEmptyInstance(); mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); resetComposingState(true /* alsoResetLastComposedWord */); @@ -330,7 +311,6 @@ public final class InputLogic { return inputTransaction; } - final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo); commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); @@ -340,13 +320,14 @@ public final class InputLogic { mSpaceState = SpaceState.PHANTOM; inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); - if (shouldShowAddToDictionaryHint) { - mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); - } else { - // If we're not showing the "Touch again to save", then update the suggestion strip. - // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. - handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); - } + // If we're not showing the "Touch again to save", then update the suggestion strip. + // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. + handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); + + StatsUtils.onPickSuggestionManually( + mSuggestedWords, suggestionInfo, mDictionaryFacilitator); + StatsUtils.onWordCommitSuggestionPickedManually( + suggestionInfo.mWord, mWordComposer.isBatchMode()); return inputTransaction; } @@ -416,14 +397,8 @@ public final class InputLogic { // The cursor has been moved : we now accept to perform recapitalization mRecapitalizeStatus.enable(); - // We moved the cursor and need to invalidate the indicator right now. - mTextDecorator.reset(); - // Remaining background color that was used for the add-to-dictionary indicator should be - // removed. - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); // We moved the cursor. If we are touching a word, we need to resume suggestion. - mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); + mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */); // Stop the last recapitalization, if started. mRecapitalizeStatus.stop(); return true; @@ -442,9 +417,8 @@ public final class InputLogic { * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} * @return the complete transaction object */ - public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event, - final int keyboardShiftMode, - // TODO: remove these arguments + public InputTransaction onCodeInput(final SettingsValues settingsValues, + @Nonnull final Event event, final int keyboardShiftMode, final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { final Event processedEvent = mWordComposer.processEvent(event); final InputTransaction inputTransaction = new InputTransaction(settingsValues, @@ -491,17 +465,14 @@ public final class InputLogic { } public void onStartBatchInput(final SettingsValues settingsValues, - // TODO: remove these arguments final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { mInputLogicHandler.onStartBatchInput(); handler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); + SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */); handler.cancelUpdateSuggestionStrip(); ++mAutoCommitSequenceNumber; mConnection.beginBatchEdit(); - if (!mWordComposer.isComposingWord()) { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - } else { + if (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 batch input at the current cursor position. @@ -557,30 +528,7 @@ public final class InputLogic { * earlier sequence number. */ private int mAutoCommitSequenceNumber = 1; - public void onUpdateBatchInput(final SettingsValues settingsValues, - final InputPointers batchPointers, - // TODO: remove these arguments - final KeyboardSwitcher keyboardSwitcher) { - if (settingsValues.mPhraseGestureEnabled) { - final SuggestedWordInfo candidate = 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 (candidate.mSourceDict.shouldAutoCommit(candidate)) { - final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2); - batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); - promotePhantomSpace(settingsValues); - mConnection.commitText(commitParts[0], 0); - mSpaceState = SpaceState.PHANTOM; - keyboardSwitcher.requestUpdatingShiftState( - getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode( - settingsValues, keyboardSwitcher.getKeyboardShiftMode())); - ++mAutoCommitSequenceNumber; - } - } - } + public void onUpdateBatchInput(final InputPointers batchPointers) { mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); } @@ -589,27 +537,25 @@ public final class InputLogic { ++mAutoCommitSequenceNumber; } - // TODO: remove this argument public void onCancelBatchInput(final LatinIME.UIHandler handler) { mInputLogicHandler.onCancelBatchInput(); handler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + SuggestedWords.getEmptyInstance(), true /* dismissGestureFloatingPreviewText */); } // TODO: on the long term, this method should become private, but it will be difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? - public void setSuggestedWords(final SuggestedWords suggestedWords, - final SettingsValues settingsValues, final LatinIME.UIHandler handler) { - if (SuggestedWords.EMPTY != suggestedWords) { - final String autoCorrection; + public void setSuggestedWords(final SuggestedWords suggestedWords) { + if (!suggestedWords.isEmpty()) { + final SuggestedWordInfo suggestedWordInfo; if (suggestedWords.mWillAutoCorrect) { - autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); + suggestedWordInfo = suggestedWords.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION); } else { // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) // because it may differ from mWordComposer.mTypedWord. - autoCorrection = suggestedWords.mTypedWord; + suggestedWordInfo = suggestedWords.mTypedWordInfo; } - mWordComposer.setAutoCorrection(autoCorrection); + mWordComposer.setAutoCorrection(suggestedWordInfo); } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; @@ -666,7 +612,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove these arguments final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { switch (event.mKeyCode) { case Constants.CODE_DELETE: @@ -683,7 +628,7 @@ public final class InputLogic { break; case Constants.CODE_CAPSLOCK: // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. + // {@link KeyboardSwitcher#onEvent(Event)}. break; case Constants.CODE_SYMBOL_SHIFT: // Note: Calling back to the keyboard on the symbol Shift key is handled in @@ -711,14 +656,13 @@ public final class InputLogic { break; case Constants.CODE_EMOJI: // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. + // {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_ALPHA_FROM_EMOJI: // Note: Switching back from Emoji keyboard to the main keyboard is being - // handled in {@link KeyboardState#onCodeInput(int,int)}. + // handled in {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_SHIFT_ENTER: - // TODO: remove this object final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler); @@ -742,7 +686,6 @@ public final class InputLogic { */ private void handleNonFunctionalEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { inputTransaction.setDidAffectContents(); switch (event.mCodePoint) { @@ -788,18 +731,7 @@ public final class InputLogic { */ private void handleNonSpecialCharacterEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { - if (!mWordComposer.isComposingWord()) { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - // In case the "add to dictionary" hint was still displayed. - // TODO: Do we really need to check if we have composing text here? - if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) { - mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); - mTextDecorator.reset(); - } - } - final int codePoint = event.mCodePoint; mSpaceState = SpaceState.NONE; if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) @@ -843,7 +775,7 @@ public final class InputLogic { // Sanity check throw new RuntimeException("Should not be composing here"); } - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { @@ -904,7 +836,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument final LatinIME.UIHandler handler) { final int codePoint = event.mCodePoint; final SettingsValues settingsValues = inputTransaction.mSettingsValues; @@ -954,12 +885,13 @@ public final class InputLogic { } if (needsPrecedingSpace) { - promotePhantomSpace(settingsValues); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } if (tryPerformDoubleSpacePeriod(event, inputTransaction)) { mSpaceState = SpaceState.DOUBLE; inputTransaction.setRequiresUpdateSuggestions(); + StatsUtils.onDoubleSpacePeriod(); } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { mSpaceState = SpaceState.SWAP_PUNCTUATION; mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); @@ -1011,7 +943,6 @@ public final class InputLogic { * @param inputTransaction The transaction in progress. */ private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, - // TODO: remove this argument, put it into settingsValues final int currentKeyboardScriptId) { mSpaceState = SpaceState.NONE; mDeleteCount++; @@ -1041,10 +972,13 @@ public final class InputLogic { mWordComposer.reset(); mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); if (!TextUtils.isEmpty(rejectedSuggestion)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); + unlearnWord(rejectedSuggestion, inputTransaction.mSettingsValues, + Constants.EVENT_REJECTION); } + StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else { mWordComposer.applyProcessedEvent(event); + StatsUtils.onBackspacePressed(1); } if (mWordComposer.isComposingWord()) { setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); @@ -1054,7 +988,24 @@ public final class InputLogic { inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { + final String lastComposedWord = mLastComposedWord.mTypedWord; revertCommit(inputTransaction, inputTransaction.mSettingsValues); + StatsUtils.onRevertAutoCorrect(); + StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode()); + // Restart suggestions when backspacing into a reverted word. This is required for + // the final corrected word to be learned, as learning only occurs when suggestions + // are active. + // + // Note: restartSuggestionsOnWordTouchedByCursor is already called for normal + // (non-revert) backspace handling. + if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + && inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { + restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, + false /* forStartInput */, currentKeyboardScriptId); + } return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1062,6 +1013,7 @@ public final class InputLogic { // 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); + StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); 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 @@ -1070,21 +1022,26 @@ public final class InputLogic { } if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { cancelDoubleSpacePeriodCountdown(); - if (mConnection.revertDoubleSpacePeriod()) { + if (mConnection.revertDoubleSpacePeriod( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { // No need to reset mSpaceState, it has already be done (that's why we // receive it as a parameter) inputTransaction.setRequiresUpdateSuggestions(); mWordComposer.setCapitalizedModeAtStartComposingTime( WordComposer.CAPS_MODE_OFF); + StatsUtils.onRevertDoubleSpacePeriod(); return; } } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { if (mConnection.revertSwapPunctuation()) { + StatsUtils.onRevertSwapPunctuation(); // Likewise return; } } + boolean hasUnlearnedWordBeingDeleted = false; + // 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 (mConnection.hasSelection()) { @@ -1094,25 +1051,36 @@ public final class InputLogic { mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); mConnection.deleteSurroundingText(numCharsDeleted, 0); + StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. - if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) { - // This should never happen. - Log.e(TAG, "Backspace when we don't know the selection position"); - } - if (inputTransaction.mSettingsValues.isBeforeJellyBean() || - inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) { - // There are two possible reasons to send a key event: either the field has + if (inputTransaction.mSettingsValues.isBeforeJellyBean() + || inputTransaction.mSettingsValues.mInputAttributes.isTypeNull() + || Constants.NOT_A_CURSOR_POSITION + == mConnection.getExpectedSelectionEnd()) { + // There are three 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. + // running in backward compatibility mode, or we don't know the cursor position. + // 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. + // As for the case where we don't know the cursor position, it can happen + // because of bugs in the framework. But the framework should know, so the next + // best thing is to leave it to whatever it thinks is best. sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + int totalDeletedLength = 1; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here because we may have already reached + // the previous word, and will lose it after next deletion. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + totalDeletedLength++; } + StatsUtils.onBackspacePressed(totalDeletedLength); } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursor == Constants.NOT_A_CODE) { @@ -1123,32 +1091,81 @@ public final class InputLogic { // catch it and have their broken interface react. If you need the keyboard // to do this, you're doing it wrong -- please fix your app. mConnection.deleteSurroundingText(1, 0); + // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDelete, 0); + int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + // If this is an accelerated (i.e., double) deletion, then we need to + // consider unlearning here because we may have already reached + // the previous word, and will lose it after next deletion. + hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + totalDeletedLength += lengthToDeleteAgain; } } + StatsUtils.onBackspacePressed(totalDeletedLength); } } - if (inputTransaction.mSettingsValues - .isSuggestionsEnabledPerUserSettings() + if (!hasUnlearnedWordBeingDeleted) { + // Consider unlearning the word being deleted (if we have not done so already). + unlearnWordBeingDeleted( + inputTransaction.mSettingsValues, currentKeyboardScriptId); + } + if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, - true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId); + false /* forStartInput */, currentKeyboardScriptId); + } + } + } + + boolean unlearnWordBeingDeleted( + final SettingsValues settingsValues,final int currentKeyboardScriptId) { + // If we just started backspacing to delete a previous word (but have not + // entered the composing state yet), unlearn the word. + // TODO: Consider tracking whether or not this word was typed by the user. + if (!mConnection.hasSelection() + && settingsValues.isSuggestionsEnabledPerUserSettings() + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + settingsValues.mSpacingAndPunctuations)) { + final TextRange range = mConnection.getWordRangeAtCursor( + settingsValues.mSpacingAndPunctuations, + currentKeyboardScriptId); + if (range == null) { + // Happens if we don't have an input connection at all. + return false; + } + final String wordBeingDeleted = range.mWord.toString(); + if (!wordBeingDeleted.isEmpty()) { + unlearnWord(wordBeingDeleted, settingsValues, + Constants.EVENT_BACKSPACE); + return true; } } + return false; + } + + void unlearnWord(final String word, final SettingsValues settingsValues, final int eventType) { + final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( + settingsValues.mSpacingAndPunctuations, 2); + final long timeStampInSeconds = TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mDictionaryFacilitator.unlearnFromUserHistory( + word, ngramContext, timeStampInSeconds, eventType); } /** @@ -1253,7 +1270,9 @@ public final class InputLogic { if (null == lastTwo) return false; final int length = lastTwo.length(); if (length < 2) return false; - if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false; + if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) { + return false; + } // We know there is a space in pos -1, and we have at least two chars. If we have only two // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine. final int firstCodePoint = @@ -1336,7 +1355,7 @@ public final class InputLogic { } private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, - final String suggestion, final PrevWordsInfo prevWordsInfo) { + final String suggestion, @Nonnull final NgramContext ngramContext) { // If correction is not enabled, we don't add words to the user history dictionary. // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. @@ -1348,7 +1367,7 @@ public final class InputLogic { final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( System.currentTimeMillis()); mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, - prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); + ngramContext, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); } public void performUpdateSuggestionStripSync(final SettingsValues settingsValues, @@ -1360,7 +1379,7 @@ public final class InputLogic { + "requested!"); } // Clear the suggestions strip. - mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY); + mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); return; } @@ -1374,14 +1393,20 @@ public final class InputLogic { new OnGetSuggestedWordsCallback() { @Override public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - final String typedWord = mWordComposer.getTypedWord(); + final String typedWordString = mWordComposer.getTypedWord(); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo( + typedWordString, "" /* prevWordsContext */, + SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE); // Show new suggestions if we have at least one. Otherwise keep the old // suggestions with the new typed word. Exception: if the length of the // typed word is <= 1 (after a deletion typically) we clear old suggestions. - if (suggestedWords.size() > 1 || typedWord.length() <= 1) { + if (suggestedWords.size() > 1 || typedWordString.length() <= 1) { holder.set(suggestedWords); } else { - holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords)); + holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords)); } } } @@ -1400,12 +1425,12 @@ public final class InputLogic { * do nothing. * * @param settingsValues the current values of the settings. - * @param shouldIncludeResumedWordInSuggestions whether to include the word on which we resume - * suggestions in the suggestion list. + * @param forStartInput whether we're doing this in answer to starting the input (as opposed + * to a cursor move, for example). In ICS, there is a platform bug that we need to work + * around only when we come here at input start time. */ - // TODO: make this private. public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, - final boolean shouldIncludeResumedWordInSuggestions, + final boolean forStartInput, // TODO: remove this argument, put it into settingsValues final int currentKeyboardScriptId) { // HACK: We may want to special-case some apps that exhibit bad behavior in case of @@ -1452,15 +1477,14 @@ public final class InputLogic { final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); - final String typedWord = range.mWord.toString(); - if (shouldIncludeResumedWordInSuggestions) { - suggestions.add(new SuggestedWordInfo(typedWord, - SuggestedWords.MAX_SUGGESTIONS + 1, - SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } - if (!isResumableWord(settingsValues, typedWord)) { + final String typedWordString = range.mWord.toString(); + final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(typedWordString, + "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS + 1, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + suggestions.add(typedWordInfo); + if (!isResumableWord(settingsValues, typedWordString)) { mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); return; } @@ -1468,9 +1492,9 @@ public final class InputLogic { for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { for (final String s : span.getSuggestions()) { ++i; - if (!TextUtils.equals(s, typedWord)) { + if (!TextUtils.equals(s, typedWordString)) { suggestions.add(new SuggestedWordInfo(s, - SuggestedWords.MAX_SUGGESTIONS - i, + "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS - i, SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, SuggestedWordInfo.NOT_A_CONFIDENCE @@ -1478,47 +1502,25 @@ public final class InputLogic { } } } - final int[] codePoints = StringUtils.toCodePointArray(typedWord); - // 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. - final PrevWordsInfo prevWordsInfo = getPrevWordsInfoFromNthPreviousWordForSuggestion( - settingsValues.mSpacingAndPunctuations, - 0 == numberOfCharsInWordBeforeCursor ? 1 : 2); + final int[] codePoints = StringUtils.toCodePointArray(typedWordString); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); mWordComposer.setCursorPositionWithinWord( - typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); - mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); + typedWordString.codePointCount(0, numberOfCharsInWordBeforeCursor)); + if (forStartInput) { + mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); + } mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); - if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) { + if (suggestions.size() <= 1) { // If there weren't any suggestion spans on this word, suggestions#size() will be 1 // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we // have no useful suggestions, so we will try to compute some for it instead. mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { @Override - public void onGetSuggestedWords( - final SuggestedWords suggestedWordsIncludingTypedWord) { - final SuggestedWords suggestedWords; - if (suggestedWordsIncludingTypedWord.size() > 1 - && !shouldIncludeResumedWordInSuggestions) { - // We were able to compute new suggestions for this word. - // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWordForRecorrection() - // method sets willAutoCorrect to false. - suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWordForRecorrection(); - } else { - // No saved suggestions, and we were unable to compute any good one - // either. Rather than displaying an empty suggestion strip, we'll - // display the original word alone in the middle. - // Since there is only one word, willAutoCorrect is false. - suggestedWords = suggestedWordsIncludingTypedWord; - } - mIsAutoCorrectionIndicatorOn = false; - mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords); }}); } else { // We found suggestion spans in the word. We'll create the SuggestedWords out of @@ -1526,14 +1528,18 @@ public final class InputLogic { // color of the word in the suggestion strip changes according to this parameter, // and false gives the correct color. final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - null /* rawSuggestions */, typedWord, false /* typedWordValid */, + null /* rawSuggestions */, typedWordInfo, false /* typedWordValid */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER); - mIsAutoCorrectionIndicatorOn = false; - mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords); } } + void doShowSuggestionsAndClearAutoCorrectionIndicator(final SuggestedWords suggestedWords) { + mIsAutoCorrectionIndicatorOn = false; + mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + } + /** * Reverts a previous commit with auto-correction. * @@ -1551,6 +1557,10 @@ public final class InputLogic { final String committedWordString = committedWord.toString(); final int cancelLength = committedWord.length(); final String separatorString = mLastComposedWord.mSeparatorString; + // If our separator is a space, we won't actually commit it, + // but set the space state to PHANTOM so that a space will be inserted + // on the next keypress + final boolean usePhantomSpace = separatorString.equals(Constants.STRING_SPACE); // We want java chars, not codepoints for the following. final int separatorLength = separatorString.length(); // TODO: should we check our saved separator against the actual contents of the text view? @@ -1569,9 +1579,11 @@ public final class InputLogic { } mConnection.deleteSurroundingText(deleteLength, 0); if (!TextUtils.isEmpty(committedWord)) { - mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); + unlearnWord(committedWordString, inputTransaction.mSettingsValues, + Constants.EVENT_REVERT); } - final String stringToCommit = originallyTypedWord + separatorString; + final String stringToCommit = originallyTypedWord + + (usePhantomSpace ? "" : separatorString); final SpannableString textToCommit = new SpannableString(stringToCommit); if (committedWord instanceof SpannableString) { final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; @@ -1583,15 +1595,11 @@ public final class InputLogic { // First, add the committed word to the list of suggestions. suggestions.add(committedWordString); for (final Object span : spans) { - // If this is a suggestion span, we check that the locale is the right one, and - // that the word is not the committed word. That should mostly be the case. + // If this is a suggestion span, we check that the word is not the committed word. + // That should mostly be the case. // Given this, we add it to the list of suggestions, otherwise we discard it. if (span instanceof SuggestionSpan) { final SuggestionSpan suggestionSpan = (SuggestionSpan)span; - if (!suggestionSpan.getLocale().equals( - inputTransaction.mSettingsValues.mLocale.toString())) { - continue; - } for (final String suggestion : suggestionSpan.getSuggestions()) { if (!suggestion.equals(committedWordString)) { suggestions.add(suggestion); @@ -1604,24 +1612,17 @@ public final class InputLogic { } } // Add the suggestion list to the list of suggestions. - textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale, - suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), + textToCommit.setSpan(new SuggestionSpan(mLatinIME /* context */, + inputTransaction.mSettingsValues.mLocale, + suggestions.toArray(new String[suggestions.size()]), 0 /* flags */, + null /* notificationTargetClass */), 0 /* start */, lastCharIndex /* end */, 0 /* flags */); } - final boolean shouldShowAddToDictionaryForTypedWord = - shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues); - if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.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. - if (shouldShowAddToDictionaryForTypedWord) { - mConnection.commitTextWithBackgroundColor(textToCommit, 1, - settingsValues.mTextHighlightColorForAddToDictionaryIndicator, - originallyTypedWordString.length()); - } else { - mConnection.commitText(textToCommit, 1); + mConnection.commitText(textToCommit, 1); + if (usePhantomSpace) { + mSpaceState = SpaceState.PHANTOM; } } else { // For languages without spaces, we revert the typed string but the cursor is flush @@ -1629,32 +1630,13 @@ public final class InputLogic { final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); mWordComposer.setComposingWord(codePoints, mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); - if (shouldShowAddToDictionaryForTypedWord) { - setComposingTextInternalWithBackgroundColor(textToCommit, 1, - settingsValues.mTextHighlightColorForAddToDictionaryIndicator, - originallyTypedWordString.length()); - } else { - setComposingTextInternal(textToCommit, 1); - } + setComposingTextInternal(textToCommit, 1); } // Don't restart suggestion yet. We'll restart if the user deletes the separator. mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - if (shouldShowAddToDictionaryForTypedWord) { - // Due to the API limitation as of L, we cannot reliably retrieve the reverted text - // when the separator causes line breaking. Until this API limitation is addressed in - // the framework, show the indicator only when the separator doesn't contain - // line-breaking characters. - if (!StringUtils.hasLineBreakCharacter(separatorString)) { - mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString, - mConnection.getExpectedSelectionStart(), - mConnection.getExpectedSelectionEnd()); - } - mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString); - } else { - // We have a separator between the word and the cursor: we should show predictions. - inputTransaction.setRequiresUpdateSuggestions(); - } + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); } /** @@ -1720,26 +1702,25 @@ public final class InputLogic { } /** - * Get information fo previous words from the nth previous word before the cursor as context + * Get n-gram context from the nth previous word before the cursor as context * for the suggestion process. * @param spacingAndPunctuations the current spacing and punctuations settings. * @param nthPreviousWord reverse index of the word to get (1-indexed) * @return the information of previous words */ - // TODO: Make this private - public PrevWordsInfo getPrevWordsInfoFromNthPreviousWordForSuggestion( + public NgramContext getNgramContextFromNthPreviousWordForSuggestion( final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) { if (spacingAndPunctuations.mCurrentLanguageHasSpaces) { // If we are typing in a language with spaces we can just look up the previous // word information from textview. - return mConnection.getPrevWordsInfoFromNthPreviousWord( + return mConnection.getNgramContextFromNthPreviousWord( spacingAndPunctuations, nthPreviousWord); - } else { - return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? - PrevWordsInfo.BEGINNING_OF_SENTENCE : - new PrevWordsInfo(new PrevWordsInfo.WordInfo( - mLastComposedWord.mCommittedWord.toString())); } + if (LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord) { + return NgramContext.BEGINNING_OF_SENTENCE; + } + return new NgramContext(new NgramContext.WordInfo( + mLastComposedWord.mCommittedWord.toString())); } /** @@ -1792,9 +1773,8 @@ public final class InputLogic { // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT. if (Constants.CODE_PERIOD == codePointBeforeCursor) { return text.substring(1); - } else { - return text; } + return text; } /** @@ -1845,21 +1825,21 @@ public final class InputLogic { * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word * and obsolete suggestions. * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( - * String, com.android.inputmethod.latin.SuggestedWords)}. - * @param typedWord The typed word as a string. + * SuggestedWordInfo, com.android.inputmethod.latin.SuggestedWords)}. + * @param typedWordInfo The typed word as a SuggestedWordInfo. * @param previousSuggestedWords The previously suggested words. * @return Obsolete suggestions with the newly typed word. */ - private SuggestedWords retrieveOlderSuggestions(final String typedWord, + static SuggestedWords retrieveOlderSuggestions(final SuggestedWordInfo typedWordInfo, final SuggestedWords previousSuggestedWords) { - final SuggestedWords oldSuggestedWords = - previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY - : previousSuggestedWords; + final SuggestedWords oldSuggestedWords = previousSuggestedWords.isPunctuationSuggestions() + ? SuggestedWords.getEmptyInstance() : previousSuggestedWords; final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = - SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); + SuggestedWords.getTypedWordAndPreviousSuggestions(typedWordInfo, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, - false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, - true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle); + typedWordInfo, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, + true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); } /** @@ -1936,14 +1916,14 @@ public final class InputLogic { } /** - * Promote a phantom space to an actual space. + * Insert an automatic space, if the options allow it. * - * 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. + * This checks the options and the text before the cursor are appropriate before inserting + * an automatic space. * * @param settingsValues the current values of the settings. */ - private void promotePhantomSpace(final SettingsValues settingsValues) { + private void insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues) { if (settingsValues.shouldInsertSpacesAutomatically() && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces && !mConnection.textBeforeCursorLooksLikeURL()) { @@ -1957,36 +1937,17 @@ public final class InputLogic { * @param suggestedWords suggestedWords to use. */ public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, - final SuggestedWords suggestedWords, - // TODO: remove this argument - final KeyboardSwitcher keyboardSwitcher) { + final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher) { final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); if (TextUtils.isEmpty(batchInputText)) { return; } mConnection.beginBatchEdit(); if (SpaceState.PHANTOM == mSpaceState) { - promotePhantomSpace(settingsValues); - } - final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); - // Commit except the last word for phrase gesture if the top suggestion is eligible for auto - // commit. - if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { - // Find the last space - final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; - if (0 != indexOfLastSpace) { - mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); - final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = - suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); - mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); - } - final String lastWord = batchInputText.substring(indexOfLastSpace); - mWordComposer.setBatchInputWord(lastWord); - setComposingTextInternal(lastWord, 1); - } else { - mWordComposer.setBatchInputWord(batchInputText); - setComposingTextInternal(batchInputText, 1); + insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); } + mWordComposer.setBatchInputWord(batchInputText); + setComposingTextInternal(batchInputText, 1); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.PHANTOM; @@ -2009,13 +1970,14 @@ public final class InputLogic { * @param settingsValues the current values of the settings. * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. */ - // TODO: Make this private public void commitTyped(final SettingsValues settingsValues, final String separatorString) { if (!mWordComposer.isComposingWord()) return; final String typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { + final boolean isBatchMode = mWordComposer.isBatchMode(); commitChosenWord(settingsValues, typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); + StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode); } } @@ -2036,9 +1998,7 @@ public final class InputLogic { * @param separator the separator that's causing the commit to happen. */ private void commitCurrentAutoCorrection(final SettingsValues settingsValues, - final String separator, - // TODO: Remove this argument. - final LatinIME.UIHandler handler) { + final String separator, final LatinIME.UIHandler handler) { // Complete any pending suggestions query first if (handler.hasPendingUpdateSuggestions()) { handler.cancelUpdateSuggestionStrip(); @@ -2052,18 +2012,19 @@ public final class InputLogic { // INPUT_STYLE_TYPING. performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); } - final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final SuggestedWordInfo autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull(); final String typedWord = mWordComposer.getTypedWord(); - final String autoCorrection = (typedAutoCorrection != null) - ? typedAutoCorrection : typedWord; - if (autoCorrection != null) { + final String stringToCommit = (autoCorrectionOrNull != null) + ? autoCorrectionOrNull.mWord : typedWord; + if (stringToCommit != null) { if (TextUtils.isEmpty(typedWord)) { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } - commitChosenWord(settingsValues, autoCorrection, + final boolean isBatchMode = mWordComposer.isBatchMode(); + commitChosenWord(settingsValues, stringToCommit, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); - if (!typedWord.equals(autoCorrection)) { + if (!typedWord.equals(stringToCommit)) { // 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 @@ -2071,8 +2032,16 @@ public final class InputLogic { // of the auto-correction flash. At this moment, the "typedWord" argument is // ignored by TextView. mConnection.commitCorrection(new CorrectionInfo( - mConnection.getExpectedSelectionEnd() - autoCorrection.length(), - typedWord, autoCorrection)); + mConnection.getExpectedSelectionEnd() - stringToCommit.length(), + typedWord, stringToCommit)); + String prevWordsContext = (autoCorrectionOrNull != null) + ? autoCorrectionOrNull.mPrevWordsContext + : ""; + StatsUtils.onAutoCorrection(typedWord, stringToCommit, isBatchMode, + mDictionaryFacilitator, prevWordsContext); + StatsUtils.onWordCommitAutoCorrect(stringToCommit, isBatchMode); + } else { + StatsUtils.onWordCommitUserTyped(stringToCommit, isBatchMode); } } } @@ -2091,20 +2060,20 @@ public final class InputLogic { final CharSequence chosenWordWithSuggestions = SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, suggestedWords); - // When we are composing word, get previous words information from the 2nd previous word - // because the 1st previous word is the word to be committed. Otherwise get previous words - // information from the 1st previous word. - final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord( + // When we are composing word, get n-gram context from the 2nd previous word because the + // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st + // previous word. + final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); mConnection.commitText(chosenWordWithSuggestions, 1); // Add the word to the user history dictionary - performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo); + performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext); // TODO: figure out here if this is an auto-correct or if the best word is actually // what user typed. Note: currently this is done much later in // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. mLastComposedWord = mWordComposer.commitWord(commitType, - chosenWordWithSuggestions, separatorString, prevWordsInfo); + chosenWordWithSuggestions, separatorString, ngramContext); } /** @@ -2118,11 +2087,8 @@ public final class InputLogic { * @param remainingTries How many times we may try again before giving up. * @return whether true if the caches were successfully reset, false otherwise. */ - // TODO: make this private public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, - final int remainingTries, - // TODO: remove these arguments - final LatinIME.UIHandler handler) { + final int remainingTries, final LatinIME.UIHandler handler) { final boolean shouldFinishComposition = mConnection.hasSelection() || !mConnection.isCursorPositionKnown(); if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( @@ -2137,30 +2103,25 @@ public final class InputLogic { } mConnection.tryFixLyingCursorPosition(); if (tryResumeSuggestions) { - // This is triggered when starting input anew, so we want to include the resumed - // word in suggestions. - handler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); + handler.postResumeSuggestions(true /* shouldDelay */); } return true; } public void getSuggestedWords(final SettingsValues settingsValues, - final ProximityInfo proximityInfo, final int keyboardShiftMode, final int inputStyle, + final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions( getActualCapsMode(settingsValues, keyboardShiftMode)); mSuggest.getSuggestedWords(mWordComposer, - getPrevWordsInfoFromNthPreviousWordForSuggestion( + getNgramContextFromNthPreviousWordForSuggestion( settingsValues.mSpacingAndPunctuations, // Get the word on which we should search the bigrams. If we are composing // a word, it's whatever is *before* the half-committed word in the buffer, // hence 2; if we aren't, we should just skip whitespace if any, so 1. mWordComposer.isComposingWord() ? 2 : 1), - proximityInfo, - new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive, - settingsValues.mPhraseGestureEnabled, - settingsValues.mAdditionalFeaturesSettingValues), + keyboard, + new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive), settingsValues.mAutoCorrectionEnabledPerUserSettings, inputStyle, sequenceNumber, callback); } @@ -2171,7 +2132,7 @@ public final class InputLogic { * * <p>Currently using this method is optional and you can still directly call * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to - * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p> + * use this method whenever possible.<p> * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> * * @param newComposingText the composing text to be set @@ -2216,70 +2177,44 @@ public final class InputLogic { mConnection.setComposingText(composingTextToBeSet, newCursorPosition); } - ////////////////////////////////////////////////////////////////////////////////////////////// - // Following methods are tentatively placed in this class for the integration with - // TextDecorator. - // TODO: Decouple things that are not related to the input logic. - ////////////////////////////////////////////////////////////////////////////////////////////// - - /** - * Sets the UI operator for {@link TextDecorator}. - * @param uiOperator the UI operator which should be associated with {@link TextDecorator}. - */ - public void setTextDecoratorUi(final TextDecoratorUiOperator uiOperator) { - mTextDecorator.setUiOperator(uiOperator); - } - /** - * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is - * called. - * @param info The wrapper object with which we can access cursor/anchor info. + * Gets an object allowing private IME commands to be sent to the + * underlying editor. + * @return An object for sending private commands to the underlying editor. */ - public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { - mTextDecorator.onUpdateCursorAnchorInfo(info); + public PrivateCommandPerformer getPrivateCommandPerformer() { + return mConnection; } /** - * Must be called when {@link InputMethodService#updateFullscreenMode} is called. - * @param isFullscreen {@code true} if the input method is in full-screen mode. - */ - public void onUpdateFullscreenMode(final boolean isFullscreen) { - mTextDecorator.notifyFullScreenMode(isFullscreen); - } - - /** - * Must be called from {@link LatinIME#addWordToUserDictionary(String)}. + * Gets the expected index of the first char of the composing span within the editor's text. + * Returns a negative value in case there appears to be no valid composing span. + * + * @see #getComposingLength() + * @see RichInputConnection#hasSelection() + * @see RichInputConnection#isCursorPositionKnown() + * @see RichInputConnection#getExpectedSelectionStart() + * @see RichInputConnection#getExpectedSelectionEnd() + * @return The expected index in Java chars of the first char of the composing span. */ - public void onAddWordToUserDictionary() { - mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); - mTextDecorator.reset(); + // TODO: try and see if we can get rid of this method. Ideally the users of this class should + // never need to know this. + public int getComposingStart() { + if (!mConnection.isCursorPositionKnown() || mConnection.hasSelection()) { + return -1; + } + return mConnection.getExpectedSelectionStart() - mWordComposer.size(); } /** - * Returns whether the add to dictionary indicator should be shown or not. - * @param lastComposedWord the last composed word information. - * @param settingsValues the current settings value. - * @return {@code true} if the commit indicator should be shown. + * Gets the expected length in Java chars of the composing span. + * May be 0 if there is no valid composing span. + * @see #getComposingStart() + * @return The expected length of the composing span. */ - private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord, - final SettingsValues settingsValues) { - if (!mConnection.isCursorAnchorInfoMonitorEnabled()) { - // We cannot help in this case because we are heavily relying on this new API. - return false; - } - if (!settingsValues.mShouldShowUiToAcceptTypedWord) { - return false; - } - if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { - return false; - } - if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) { - return false; - } - if (!mDictionaryFacilitator.isUserDictionaryEnabled()) { - return false; - } - return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord, - true /* ignoreCase */); + // TODO: try and see if we can get rid of this method. Ideally the users of this class should + // never need to know this. + public int getComposingLength() { + return mWordComposer.size(); } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java index c6f83d0b9..ddc4ad99c 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java @@ -21,11 +21,10 @@ import android.os.HandlerThread; import android.os.Message; import com.android.inputmethod.compat.LooperCompatUtils; -import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; +import com.android.inputmethod.latin.common.InputPointers; /** * A helper to manage deferred tasks for the input logic. @@ -62,7 +61,7 @@ class InputLogicHandler implements Handler.Callback { final OnGetSuggestedWordsCallback callback) {} }; - private InputLogicHandler() { + InputLogicHandler() { mNonUIThreadHandler = null; mLatinIME = null; mInputLogic = null; @@ -134,30 +133,38 @@ class InputLogicHandler implements Handler.Callback { return; } mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); + final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput); + } + }; getSuggestedWords(isTailBatchInput ? SuggestedWords.INPUT_STYLE_TAIL_BATCH - : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(SuggestedWords suggestedWords) { - // We're now inside the callback. This always runs on the Non-UI thread, - // no matter what thread updateBatchInput was originally called on. - if (suggestedWords.isEmpty()) { - // Use old suggestions if we don't have any new ones. - // Previous suggestions are found in InputLogic#mSuggestedWords. - // Since these are the most recent ones and we just recomputed - // new ones to update them, then the previous ones are there. - suggestedWords = mInputLogic.mSuggestedWords; - } - mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, - isTailBatchInput /* dismissGestureFloatingPreviewText */); - if (isTailBatchInput) { - mInBatchInput = false; - // The following call schedules onEndBatchInputInternal - // to be called on the UI thread. - mLatinIME.mHandler.showTailBatchInputResult(suggestedWords); - } - } - }); + : SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, callback); + } + } + + void showGestureSuggestionsWithPreviewVisuals(final SuggestedWords suggestedWordsForBatchInput, + final boolean isTailBatchInput) { + final SuggestedWords suggestedWordsToShowSuggestions; + // We're now inside the callback. This always runs on the Non-UI thread, + // no matter what thread updateBatchInput was originally called on. + if (suggestedWordsForBatchInput.isEmpty()) { + // Use old suggestions if we don't have any new ones. + // Previous suggestions are found in InputLogic#mSuggestedWords. + // Since these are the most recent ones and we just recomputed + // new ones to update them, then the previous ones are there. + suggestedWordsToShowSuggestions = mInputLogic.mSuggestedWords; + } else { + suggestedWordsToShowSuggestions = suggestedWordsForBatchInput; + } + mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWordsToShowSuggestions, + isTailBatchInput /* dismissGestureFloatingPreviewText */); + if (isTailBatchInput) { + mInBatchInput = false; + // The following call schedules onEndBatchInputInternal + // to be called on the UI thread. + mLatinIME.mHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions); } } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java new file mode 100644 index 000000000..42eaa9c82 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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.Bundle; + +/** + * Provides an interface matching + * {@link android.view.inputmethod.InputConnection#performPrivateCommand(String,Bundle)}. + */ +public interface PrivateCommandPerformer { + /** + * API to send private commands from an input method to its connected + * editor. This can be used to provide domain-specific features that are + * only known between certain input methods and their clients. + * + * @param action Name of the command to be performed. This must be a scoped + * name, i.e. prefixed with a package name you own, so that + * different developers will not create conflicting commands. + * @param data Any data to include with the command. + * @return true if the command was sent (regardless of whether the + * associated editor understood it), false if the input connection is no + * longer valid. + */ + boolean performPrivateCommand(String action, Bundle data); +} |