diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/LatinIME.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/LatinIME.java | 750 |
1 files changed, 532 insertions, 218 deletions
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index ffe317161..270dc4c06 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -31,6 +31,7 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Rect; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; @@ -45,11 +46,13 @@ import android.text.InputType; import android.text.TextUtils; import android.text.style.SuggestionSpan; import android.util.Log; +import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; @@ -71,10 +74,12 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; 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.personalization.PersonalizationDictionaryHelper; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; +import com.android.inputmethod.latin.personalization.PersonalizationHelper; import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary; import com.android.inputmethod.latin.settings.Settings; @@ -82,6 +87,7 @@ import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.latin.utils.ApplicationUtils; +import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CollectionUtils; @@ -90,11 +96,11 @@ import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; -import com.android.inputmethod.latin.utils.PositionalInfoForUserDictPendingAddition; import com.android.inputmethod.latin.utils.RecapitalizeStatus; import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; import com.android.inputmethod.latin.utils.TextRange; +import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; import com.android.inputmethod.research.ResearchLogger; import java.io.FileDescriptor; @@ -124,6 +130,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; + // TODO: Set this value appropriately. + private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; + /** * The name of the scheme used by the Package Manager to warn of a new package installation, * replacement or removal. @@ -151,8 +160,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final Settings mSettings; - private View mInputView; - private int mInputViewMinHeight; + private View mExtractArea; + private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; // Never null private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; @@ -172,11 +181,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private UserBinaryDictionary mUserDictionary; private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary; private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary; + private PersonalizationDictionary mPersonalizationDictionary; private boolean mIsUserDictionaryAvailable; private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private PositionalInfoForUserDictPendingAddition - mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); private final RichInputConnection mConnection = new RichInputConnection(this); private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); @@ -191,7 +199,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private boolean mExpectingUpdateSelection; private int mDeleteCount; private long mLastKeyTime; - private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); + 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; @@ -212,6 +223,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final boolean mIsHardwareAcceleratedDrawingEnabled; public final UIHandler mHandler = new UIHandler(this); + private InputUpdater mInputUpdater; public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { private static final int MSG_UPDATE_SHIFT_STATE = 0; @@ -220,8 +232,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int MSG_REOPEN_DICTIONARIES = 5; + private static final int MSG_ON_END_BATCH_INPUT = 6; + private static final int MSG_RESET_CACHES = 7; + private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; + private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; + private static final int ARG2_WITHOUT_TYPED_WORD = 0; + private static final int ARG2_WITH_TYPED_WORD = 1; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; @@ -254,8 +272,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen switcher.updateShiftState(); break; case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: - latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, - msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); + if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { + if (msg.arg2 == ARG2_WITH_TYPED_WORD) { + final Pair<SuggestedWords, String> p = + (Pair<SuggestedWords, String>) msg.obj; + latinIme.showSuggestionStripWithTypedWord(p.first, p.second); + } else { + latinIme.showSuggestionStrip((SuggestedWords) msg.obj); + } + } else { + latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, + msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); + } break; case MSG_RESUME_SUGGESTIONS: latinIme.restartSuggestionsOnWordTouchedByCursor(); @@ -267,6 +295,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // get any suggestions. Wait one frame. postUpdateSuggestionStrip(); break; + case MSG_ON_END_BATCH_INPUT: + latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj); + break; + case MSG_RESET_CACHES: + latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */, + msg.arg2 /* remainingTries */); + break; } } @@ -283,6 +318,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); } + public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { + removeMessages(MSG_RESET_CACHES); + sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0, + remainingTries, null)); + } + public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } @@ -308,9 +349,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean dismissGestureFloatingPreviewText) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); final int arg1 = dismissGestureFloatingPreviewText - ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0; - obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords) - .sendToTarget(); + ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT + : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, + ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); + } + + public void showSuggestionStrip(final SuggestedWords suggestedWords) { + removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, + ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); + } + + // TODO: Remove this method. + public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, + final String typedWord) { + removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, + ARG2_WITH_TYPED_WORD, + new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget(); + } + + public void onEndBatchInput(final SuggestedWords suggestedWords) { + obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget(); } public void startDoubleSpacePeriodTimer() { @@ -505,6 +566,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final IntentFilter newDictFilter = new IntentFilter(); newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + + mInputUpdater = new InputUpdater(this); } // Has to be package-visible for unit tests @@ -560,10 +623,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - mUserHistoryPredictionDictionary = PersonalizationDictionaryHelper + mUserHistoryPredictionDictionary = PersonalizationHelper .getUserHistoryPredictionDictionary(this, localeStr, prefs); newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary); - mPersonalizationPredictionDictionary = PersonalizationDictionaryHelper + mPersonalizationDictionary = PersonalizationHelper + .getPersonalizationDictionary(this, localeStr, prefs); + newSuggest.setPersonalizationDictionary(mPersonalizationDictionary); + mPersonalizationPredictionDictionary = PersonalizationHelper .getPersonalizationPredictionDictionary(this, localeStr, prefs); newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary); @@ -635,8 +701,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ResearchLogger.getInstance().onDestroy(); } unregisterReceiver(mDictionaryPackInstallReceiver); + PersonalizationDictionarySessionRegister.onDestroy(this); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); + if (mInputUpdater != null) { + mInputUpdater.onDestroy(); + mInputUpdater = null; + } super.onDestroy(); } @@ -663,25 +734,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } - private void setInputViewMinHeight(final int minHeight) { - if (mInputView != null && mInputViewMinHeight != minHeight) { - mInputView.setMinimumHeight(minHeight); - mInputViewMinHeight = minHeight; - } - } - @Override - public void setInputView(final View inputView) { - super.setInputView(inputView); - mInputView = inputView; - setInputViewMinHeight(0); - mSuggestionStripView = (SuggestionStripView)inputView.findViewById( - R.id.suggestion_strip_view); - if (mSuggestionStripView != null) { - mSuggestionStripView.setListener(this, inputView); - } + public void setInputView(final View view) { + super.setInputView(view); + mExtractArea = getWindow().getWindow().getDecorView() + .findViewById(android.R.id.extractArea); + mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); + mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); + if (mSuggestionStripView != null) + mSuggestionStripView.setListener(this, view); if (LatinImeLogger.sVISUALDEBUG) { - inputView.setBackgroundColor(0x10FF0000); + mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); } } @@ -800,7 +863,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // span, so we should reset our state unconditionally, even if restarting is true. mEnteredText = null; resetComposingState(true /* alsoResetLastComposedWord */); - if (isDifferentTextField) mHandler.postResumeSuggestions(); mDeleteCount = 0; mSpaceState = SPACE_STATE_NONE; mRecapitalizeStatus.deactivate(); @@ -819,8 +881,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } mSuggestedWords = SuggestedWords.EMPTY; - mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart, - false /* shouldFinishComposition */); + // 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. + if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart, + false /* shouldFinishComposition */)) { + // We try resetting the caches up to 5 times before giving up. + mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); + } else { + if (isDifferentTextField) mHandler.postResumeSuggestions(); + } if (isDifferentTextField) { mainKeyboardView.closing(); @@ -847,6 +917,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mLastSelectionStart = editorInfo.initialSelStart; 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(); mHandler.cancelUpdateSuggestionStrip(); mHandler.cancelDoubleSpacePeriodTimer(); @@ -861,22 +934,64 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); - // If we have a user dictionary addition in progress, we should check now if we should - // replace the previously committed string with the word that has actually been added - // to the user dictionary. - if (null != mPositionalInfoForUserDictPendingAddition - && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( - mConnection, editorInfo, mLastSelectionEnd, currentLocale)) { - mPositionalInfoForUserDictPendingAddition = null; - } - // If tryReplaceWithActualWord returns false, we don't know what word was - // added to the user dictionary yet, so we keep the data and defer processing. The word will - // be replaced when the user dictionary reports back with the actual word, which ends - // up calling #onWordAddedToUserDictionary() in this class. + initPersonalizationDebugSettings(currentSettingsValues); if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } + /** + * Try to get the text from the editor to expose lies the framework may have been + * telling us. Concretely, when the device rotates, the frameworks tells us about where the + * cursor used to be initially in the editor at the time it first received the focus; this + * may be completely different from the place it is upon rotation. Since we don't have any + * means to get the real value, try at least to ask the text view for some characters and + * detect the most damaging cases: when the cursor position is declared to be much smaller + * than it really is. + */ + private void tryFixLyingCursorPosition() { + final CharSequence textBeforeCursor = + mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + if (null == textBeforeCursor) { + mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION; + } else { + final int textLength = textBeforeCursor.length(); + if (textLength > mLastSelectionStart + || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE + && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { + mLastSelectionStart = textLength; + // We can't figure out the value of mLastSelectionEnd :( + // But at least if it's smaller than mLastSelectionStart something is wrong + if (mLastSelectionStart > mLastSelectionEnd) { + mLastSelectionEnd = mLastSelectionStart; + } + } + } + } + + // Initialization of personalization debug settings. This must be called inside + // onStartInputView. + private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) { + if (mUseOnlyPersonalizationDictionaryForDebug + != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) { + // Only for debug + initSuggest(); + mUseOnlyPersonalizationDictionaryForDebug = + currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug; + } + + if (mBoostPersonalizationDictionaryForDebug != + currentSettingsValues.mBoostPersonalizationDictionaryForDebug) { + // Only for debug + mBoostPersonalizationDictionaryForDebug = + currentSettingsValues.mBoostPersonalizationDictionaryForDebug; + if (mBoostPersonalizationDictionaryForDebug) { + UserHistoryForgettingCurveUtils.boostMaxFreqForDebug(); + } else { + UserHistoryForgettingCurveUtils.resetMaxFreqForDebug(); + } + } + } + // Callback for the TargetPackageInfoGetterTask @Override public void onTargetPackageInfoKnown(final PackageInfo info) { @@ -1007,7 +1122,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // argument as true. 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.resetCachesUponCursorMove(newSelStart, + mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, false /* shouldFinishComposition */); } @@ -1133,11 +1248,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSuggestionStripView.setVisibility( shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); } - if (shouldShowSuggestions && mainKeyboardView != null) { - final int remainingHeight = getWindow().getWindow().getDecorView().getHeight() - - mainKeyboardView.getHeight() - mSuggestionStripView.getHeight(); - mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); - } } } @@ -1145,37 +1255,69 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); } + private int getAdjustedBackingViewHeight() { + final int currentHeight = mKeyPreviewBackingView.getHeight(); + if (currentHeight > 0) { + return currentHeight; + } + + final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); + if (visibleKeyboardView == null) { + return 0; + } + // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!! + // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + final int keyboardHeight = visibleKeyboardView.getHeight(); + final int suggestionsHeight = mSuggestionStripView.getHeight(); + final int displayHeight = getResources().getDisplayMetrics().heightPixels; + final Rect rect = new Rect(); + mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect); + final int notificationBarHeight = rect.top; + final int remainingHeight = displayHeight - notificationBarHeight - suggestionsHeight + - keyboardHeight; + + final LayoutParams params = mKeyPreviewBackingView.getLayoutParams(); + params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight); + mKeyPreviewBackingView.setLayoutParams(params); + return params.height; + } + @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (mainKeyboardView == null || mSuggestionStripView == null) { + final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); + if (visibleKeyboardView == null || mSuggestionStripView == null) { return; } - // This method is never called when in fullscreen mode. - // The contentTop is the top coordinate of the keyboard. The application behind will be - // resized/panned above this coordibnate to be able to show an input field. - final int contentTop = mInputView.getHeight() - mainKeyboardView.getHeight(); - final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.VISIBLE) - ? mSuggestionStripView.getHeight() : 0; - // The visibleTop is the top coordinates of the visible part of this IME. The application - // behind will never be resized, but may be panned or scrolled. - final int visibleTop = mainKeyboardView.isShowingMoreKeysPanel() ? 0 - : contentTop - suggestionsHeight; - outInsets.contentTopInsets = contentTop; - outInsets.visibleTopInsets = visibleTop; + final int adjustedBackingHeight = getAdjustedBackingViewHeight(); + final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE); + final int backingHeight = backingGone ? 0 : adjustedBackingHeight; + // In fullscreen mode, the height of the extract area managed by InputMethodService should + // be considered. + // See {@link android.inputmethodservice.InputMethodService#onComputeInsets}. + final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0; + final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0 + : mSuggestionStripView.getHeight(); + final int extraHeight = extractHeight + backingHeight + suggestionsHeight; + int visibleTopY = extraHeight; // Need to set touchable region only if input view is being shown - if (mainKeyboardView.isShown()) { - final int touchLeft = 0; - final int touchTop = visibleTop; - final int touchRight = touchLeft + mainKeyboardView.getWidth(); - final int touchBottom = contentTop + mainKeyboardView.getHeight() + if (visibleKeyboardView.isShown()) { + // Note that the height of Emoji layout is the same as the height of the main keyboard + // and the suggestion strip + if (mKeyboardSwitcher.isShowingEmojiKeyboard() + || mSuggestionStripView.getVisibility() == View.VISIBLE) { + visibleTopY -= suggestionsHeight; + } + final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY; + final int touchWidth = visibleKeyboardView.getWidth(); + final int touchHeight = visibleKeyboardView.getHeight() + extraHeight // Extend touchable region below the keyboard. + EXTENDED_TOUCHABLE_REGION_HEIGHT; - // The touch event on touchableRegion will be delivered to this IME. - outInsets.touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; + outInsets.touchableRegion.set(0, touchY, touchWidth, touchHeight); } + outInsets.contentTopInsets = visibleTopY; + outInsets.visibleTopInsets = visibleTopY; } @Override @@ -1198,11 +1340,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void updateFullscreenMode() { super.updateFullscreenMode(); - if (!isFullscreenMode()) { - // Expand the input view to cover entire display to be able to show key previews and - // more suggestions view that may be displayed above the keyboard. - setInputViewMinHeight(getResources().getDisplayMetrics().heightPixels); - } + + if (mKeyPreviewBackingView == null) return; + // In fullscreen mode, no need to have extra space to show the key preview. + // If not, we should have extra space above the keyboard to show the key preview. + mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); } // This will reset the whole input state to the starting state. It will clear @@ -1216,7 +1358,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else { setSuggestedWords(settingsValues.mSuggestPuncList, false); } - mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition); + mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition, + shouldFinishComposition); } private void resetComposingState(final boolean alsoResetLastComposedWord) { @@ -1320,7 +1463,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen || 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_CLOSING_ANGLE_BRACKET + || codePoint == Constants.CODE_PLUS; } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is @@ -1329,7 +1473,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void addWordToUserDictionary(final String word) { if (TextUtils.isEmpty(word)) { // Probably never supposed to happen, but just in case. - mPositionalInfoForUserDictPendingAddition = null; return; } final String wordToEdit; @@ -1341,22 +1484,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mUserDictionary.addWordToUserDictionary(wordToEdit); } - public void onWordAddedToUserDictionary(final String newSpelling) { - // If word was added but not by us, bail out - if (null == mPositionalInfoForUserDictPendingAddition) return; - if (mWordComposer.isComposingWord()) { - // We are late... give up and return - mPositionalInfoForUserDictPendingAddition = null; - return; - } - mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling); - if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord( - mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd, - mSubtypeSwitcher.getCurrentSubtypeLocale())) { - mPositionalInfoForUserDictPendingAddition = null; - } - } - private void onSettingsKeyPressed() { if (isShowingOptionDialog()) return; showSubtypeSelectorAndSettings(); @@ -1496,7 +1623,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen handleLanguageSwitchKey(); break; case Constants.CODE_EMOJI: - // TODO: Implement emoji keyboard switch. + // Note: Switching emoji keyboard is being handled in + // {@link KeyboardState#onCodeInput(int,int)}. break; case Constants.CODE_ENTER: final EditorInfo editorInfo = getCurrentInputEditorInfo(); @@ -1612,7 +1740,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onStartBatchInput() { - BatchInputUpdater.getInstance().onStartBatchInput(this); + mInputUpdater.onStartBatchInput(); mHandler.cancelUpdateSuggestionStrip(); mConnection.beginBatchEdit(); final SettingsValues settingsValues = mSettings.getCurrent(); @@ -1652,46 +1780,41 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); } - private static final class BatchInputUpdater implements Handler.Callback { + private static final class InputUpdater implements Handler.Callback { private final Handler mHandler; - private LatinIME mLatinIme; + private final LatinIME mLatinIme; private final Object mLock = new Object(); private boolean mInBatchInput; // synchronized using {@link #mLock}. - private BatchInputUpdater() { + private InputUpdater(final LatinIME latinIme) { final HandlerThread handlerThread = new HandlerThread( - BatchInputUpdater.class.getSimpleName()); + InputUpdater.class.getSimpleName()); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper(), this); - } - - // Initialization-on-demand holder - private static final class OnDemandInitializationHolder { - public static final BatchInputUpdater sInstance = new BatchInputUpdater(); - } - - public static BatchInputUpdater getInstance() { - return OnDemandInitializationHolder.sInstance; + mLatinIme = latinIme; } private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; + private static final int MSG_GET_SUGGESTED_WORDS = 2; @Override public boolean handleMessage(final Message msg) { switch (msg.what) { - case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: - updateBatchInput((InputPointers)msg.obj); - break; + case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: + updateBatchInput((InputPointers)msg.obj); + break; + case MSG_GET_SUGGESTED_WORDS: + mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj); + break; } return true; } // Run in the UI thread. - public void onStartBatchInput(final LatinIME latinIme) { + public void onStartBatchInput() { synchronized (mLock) { mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); mInBatchInput = true; - mLatinIme = latinIme; mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); } @@ -1704,9 +1827,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Batch input has ended or canceled while the message was being delivered. return; } - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); + + getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, false /* dismissGestureFloatingPreviewText */); + } + }); } } @@ -1729,35 +1857,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } // Run in the UI thread. - public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { - synchronized (mLock) { - mInBatchInput = false; - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, true /* dismissGestureFloatingPreviewText */); - return suggestedWords; + public void onEndBatchInput(final InputPointers batchPointers) { + synchronized(mLock) { + getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + mInBatchInput = false; + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, + true /* dismissGestureFloatingPreviewText */); + mLatinIme.mHandler.onEndBatchInput(suggestedWords); + } + }); } } // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to // be synchronized. - private SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) { + private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, + final OnGetSuggestedWordsCallback callback) { mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); - final SuggestedWords suggestedWords = - mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE); - final int suggestionCount = suggestedWords.size(); - if (suggestionCount <= 1) { - final String mostProbableSuggestion = (suggestionCount == 0) ? null - : suggestedWords.getWord(0); - return mLatinIme.getOlderSuggestions(mostProbableSuggestion); - } - return suggestedWords; + mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(SuggestedWords suggestedWords) { + final int suggestionCount = suggestedWords.size(); + if (suggestionCount <= 1) { + final String mostProbableSuggestion = (suggestionCount == 0) ? null + : suggestedWords.getWord(0); + callback.onGetSuggestedWords( + mLatinIme.getOlderSuggestions(mostProbableSuggestion)); + } + callback.onGetSuggestedWords(suggestedWords); + } + }); + } + + public void getSuggestedWords(final int sessionId, + final OnGetSuggestedWordsCallback callback) { + mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget(); + } + + private void onDestroy() { + mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS); + mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + mHandler.getLooper().quit(); } } + // This method must run in UI Thread. private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { - showSuggestionStrip(suggestedWords, null); + showSuggestionStrip(suggestedWords); final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); mainKeyboardView.showGestureFloatingPreviewText(suggestedWords); if (dismissGestureFloatingPreviewText) { @@ -1767,24 +1917,48 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onUpdateBatchInput(final InputPointers batchPointers) { - BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers); + if (mSettings.getCurrent().mPhraseGestureEnabled) { + final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); + if (null != candidate) { + 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; + mKeyboardSwitcher.updateShiftState(); + mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); + } + } + } + mInputUpdater.onUpdateBatchInput(batchPointers); } - @Override - public void onEndBatchInput(final InputPointers batchPointers) { - final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput( - batchPointers); + // This method must run in UI Thread. + public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) { final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); if (TextUtils.isEmpty(batchInputText)) { return; } - mWordComposer.setBatchInputWord(batchInputText); mConnection.beginBatchEdit(); if (SPACE_STATE_PHANTOM == mSpaceState) { promotePhantomSpace(); } - mConnection.setComposingText(batchInputText, 1); + 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); + showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); + } + final String lastWord = batchInputText.substring(indexOfLastSpace); + mWordComposer.setBatchInputWord(lastWord); + mConnection.setComposingText(lastWord, 1); + } else { + mWordComposer.setBatchInputWord(batchInputText); + mConnection.setComposingText(batchInputText, 1); + } mExpectingUpdateSelection = true; mConnection.endBatchEdit(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { @@ -1795,6 +1969,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.updateShiftState(); } + @Override + public void onEndBatchInput(final InputPointers batchPointers) { + mInputUpdater.onEndBatchInput(batchPointers); + } + private String specificTldProcessingOnTextInput(final String text) { if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD || !Character.isLetter(text.charAt(1))) { @@ -1830,7 +2009,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onCancelBatchInput() { - BatchInputUpdater.getInstance().onCancelBatchInput(); + mInputUpdater.onCancelBatchInput(); } private void handleBackspace(final int spaceState) { @@ -1948,7 +2127,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { + if (currentSettings.isSuggestionsRequested(mDisplayOrientation) + && currentSettings.mCurrentLanguageHasSpaces) { restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); } // We just removed a character. We need to update the auto-caps state. @@ -1977,6 +2157,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen 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. @@ -1996,17 +2179,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen resetEntireInputState(mLastSelectionStart); isComposingWord = false; } - // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several - // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI - // thread here. - if (!isComposingWord && currentSettings.isWordCodePoint(primaryCode) + // 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) && - !mConnection.isCursorTouchingWord(currentSettings)) { + // 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. The idea here is, single quote is not a - // separator and it should be treated as a normal character, except in the first - // position where it should not start composing a word. - isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode); + // 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 @@ -2085,20 +2277,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyboardSwitcher.updateShiftState(); } - // Returns true if we did an autocorrection, false otherwise. + // 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); } - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mWordComposer.isComposingWord()) { + if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing if (currentSettings.mCorrectionEnabled) { - // TODO: maybe cache Strings in an <String> sparse array or something - commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1)); + final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR + : new String(new int[] { primaryCode }, 0, 1); + commitCurrentAutoCorrection(separator); didAutoCorrect = true; } else { commitTyped(new String(new int[]{primaryCode}, 0, 1)); @@ -2115,7 +2311,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } - sendKeyCodePoint(primaryCode); + + if (!shouldAvoidSendingCode) { + sendKeyCodePoint(primaryCode); + } if (Constants.CODE_SPACE == primaryCode) { if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { @@ -2245,34 +2444,60 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } - final SuggestedWords suggestedWords = - getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING); - final String typedWord = mWordComposer.getTypedWord(); - showSuggestionStrip(suggestedWords, typedWord); + final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); + getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + holder.set(suggestedWords); + } + } + ); + + // This line may cause the current thread to wait. + final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT); + if (suggestedWords != null) { + showSuggestionStrip(suggestedWords); + } } - private SuggestedWords getSuggestedWords(final int sessionId) { + private void getSuggestedWords(final int sessionId, + final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final Suggest suggest = mSuggest; if (keyboard == null || suggest == null) { - return SuggestedWords.EMPTY; + callback.onGetSuggestedWords(SuggestedWords.EMPTY); + return; } // 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. - // TODO: this is slow (2-way IPC) - we should probably cache this instead. final SettingsValues currentSettings = mSettings.getCurrent(); - final String prevWord = - mConnection.getNthPreviousWord(currentSettings.mWordSeparators, - mWordComposer.isComposingWord() ? 2 : 1); - return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), - currentSettings.mBlockPotentiallyOffensive, - currentSettings.mCorrectionEnabled, sessionId); + final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; + final String prevWord; + if (currentSettings.mCurrentLanguageHasSpaces) { + // If we are typing in a language with spaces we can just look up the previous + // word from textview. + prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, + mWordComposer.isComposingWord() ? 2 : 1); + } else { + prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null + : mLastComposedWord.mCommittedWord; + } + suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), + currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, + additionalFeaturesOptions, sessionId, callback); } - private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) { - return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(), - getSuggestedWords(sessionId)); + private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId, + final OnGetSuggestedWordsCallback callback) { + mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(SuggestedWords suggestedWords) { + callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( + mWordComposer.getTypedWord(), suggestedWords)); + } + }); } private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, @@ -2312,25 +2537,42 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen false /* isPrediction */); } - private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords.isEmpty()) { - clearSuggestionStrip(); - return; - } + private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { + if (suggestedWords.isEmpty()) return; final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { - autoCorrection = suggestedWords.getWord(1); + autoCorrection = suggestedWords.getWord(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 = typedWord; } mWordComposer.setAutoCorrection(autoCorrection); - final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); - setSuggestedWords(suggestedWords, isAutoCorrection); - setAutoCorrectionIndicator(isAutoCorrection); - setSuggestionStripShown(isSuggestionsStripVisible()); } - private void commitCurrentAutoCorrection(final String separatorString) { + private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, + final String typedWord) { + if (suggestedWords.isEmpty()) { + clearSuggestionStrip(); + return; + } + setAutoCorrection(suggestedWords, typedWord); + final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); + setSuggestedWords(suggestedWords, isAutoCorrection); + setAutoCorrectionIndicator(isAutoCorrection); + setSuggestionStripShown(isSuggestionsStripVisible()); + } + + private void showSuggestionStrip(final SuggestedWords suggestedWords) { + if (suggestedWords.isEmpty()) { + clearSuggestionStrip(); + return; + } + showSuggestionStripWithTypedWord(suggestedWords, + suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); + } + + private void commitCurrentAutoCorrection(final String separator) { // Complete any pending suggestions query first if (mHandler.hasPendingUpdateSuggestions()) { updateSuggestionStrip(); @@ -2346,16 +2588,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } if (mSettings.isInternal()) { LatinImeLoggerUtils.onAutoCorrection( - typedWord, autoCorrection, separatorString, mWordComposer); + typedWord, autoCorrection, separator, mWordComposer); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final SuggestedWords suggestedWords = mSuggestedWords; ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, - separatorString, mWordComposer.isBatchMode(), suggestedWords); + separator, mWordComposer.isBatchMode(), suggestedWords); } mExpectingUpdateSelection = true; commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, - separatorString); + 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 @@ -2430,7 +2672,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, - suggestionInfo.mSourceDict); + suggestionInfo.mSourceDict.mDictType); } mConnection.endBatchEdit(); // Don't allow cancellation of manual pick @@ -2525,6 +2767,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return prevWord; } + private boolean isResumableWord(final String word, final SettingsValues settings) { + final int firstCodePoint = word.codePointAt(0); + return settings.isWordCodePoint(firstCodePoint) + && Constants.CODE_SINGLE_QUOTE != firstCodePoint + && Constants.CODE_DASH != firstCodePoint; + } + /** * Check if the cursor is touching a word. If so, restart suggestions on this word, else * do nothing. @@ -2534,6 +2783,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // recorrection. This is a temporary, stopgap measure that will be removed later. // TODO: remove this. if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; + // A simple way to test for support from the TextView. + if (!isSuggestionsStripVisible()) return; + // Recorrection is not supported in languages without spaces because we don't know + // 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 we don't know the cursor location, return. @@ -2543,12 +2797,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final TextRange range = 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; final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); final String typedWord = range.mWord.toString(); + if (!isResumableWord(typedWord, currentSettings)) return; int i = 0; for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { for (final String s : span.getSuggestions()) { @@ -2556,7 +2812,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!TextUtils.equals(s, typedWord)) { suggestions.add(new SuggestedWordInfo(s, SuggestionStripView.MAX_SUGGESTIONS - i, - SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED)); + SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE + /* autoCommitFirstWordConfidence */)); } } } @@ -2566,41 +2825,56 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mConnection.setComposingRegion( mLastSelectionStart - numberOfCharsInWordBeforeCursor, mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); - final SuggestedWords suggestedWords; 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. - final SuggestedWords suggestedWordsIncludingTypedWord = - getSuggestedWords(Suggest.SESSION_TYPING); - if (suggestedWordsIncludingTypedWord.size() > 1) { - // 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 #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. - suggestedWords = - suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); - } 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; - } + mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords( + final SuggestedWords suggestedWordsIncludingTypedWord) { + final SuggestedWords suggestedWords; + if (suggestedWordsIncludingTypedWord.size() > 1) { + // 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 #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to + // false. + suggestedWords = suggestedWordsIncludingTypedWord + .getSuggestedWordsExcludingTypedWord(); + } 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; + } + // We need to pass typedWord because mWordComposer.mTypedWord may differ from + // typedWord. + unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, + typedWord); + }}); } else { // We found suggestion spans in the word. We'll create the SuggestedWords out of // them, and make willAutoCorrect false. - suggestedWords = new SuggestedWords(suggestions, + final SuggestedWords suggestedWords = new SuggestedWords(suggestions, true /* typedWordValid */, false /* willAutoCorrect */, false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */); + // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord. + unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord); } + } + public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( + final SuggestedWords suggestedWords, final String typedWord) { // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. - // We never want to auto-correct on a resumed suggestion. Please refer to the three - // places above where suggestedWords is affected. We also need to reset - // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it. - // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition) + // We never want to auto-correct on a resumed suggestion. Please refer to the three places + // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected. + // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching + // the text to adapt it. + // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) mIsAutoCorrectionIndicatorOn = false; - showSuggestionStrip(suggestedWords, typedWord); + mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); } /** @@ -2630,6 +2904,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen 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. + * This method handles the retry, and re-schedules a new retry if we still can't access. + * We only retry up to 5 times before giving up. + * + * @param tryResumeSuggestions Whether we should resume suggestions or not. + * @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, false)) { + if (0 < remainingTries) { + mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1); + } + return; + } + tryFixLyingCursorPosition(); + if (tryResumeSuggestions) mHandler.postResumeSuggestions(); + } + private void revertCommit() { final String previousWord = mLastComposedWord.mPrevWord; final String originallyTypedWord = mLastComposedWord.mTypedWord; @@ -2656,7 +2951,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord); } - mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1); + 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, mKeyboardSwitcher.getKeyboard()); + mConnection.setComposingText(stringToCommit, 1); + } if (mSettings.isInternal()) { LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); @@ -2674,7 +2980,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // This essentially inserts a space, and that's it. public void promotePhantomSpace() { - if (mSettings.getCurrent().shouldInsertSpacesAutomatically() + final SettingsValues currentSettings = mSettings.getCurrent(); + if (currentSettings.shouldInsertSpacesAutomatically() + && currentSettings.mCurrentLanguageHasSpaces && !mConnection.textBeforeCursorLooksLikeURL()) { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_promotePhantomSpace(); @@ -2887,6 +3195,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mSuggest.hasMainDictionary(); } + // 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); + } + public void debugDumpStateAndCrashWithException(final String context) { final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) |