Newer
Older
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.Color;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.SuggestionSpan;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import org.futo.inputmethod.accessibility.AccessibilityUtils;
import org.futo.inputmethod.compat.SuggestionSpanUtils;
import org.futo.inputmethod.event.Event;
import org.futo.inputmethod.event.InputTransaction;
import org.futo.inputmethod.keyboard.KeyDetector;
import org.futo.inputmethod.keyboard.Keyboard;
import org.futo.inputmethod.keyboard.KeyboardSwitcher;
import org.futo.inputmethod.latin.Dictionary;
import org.futo.inputmethod.latin.DictionaryFacilitator;
import org.futo.inputmethod.latin.LastComposedWord;
Aleksandras Kostarevas
committed
import org.futo.inputmethod.latin.LatinIMELegacy;
import org.futo.inputmethod.latin.NgramContext;
import org.futo.inputmethod.latin.RichInputConnection;
import org.futo.inputmethod.latin.Suggest;
import org.futo.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import org.futo.inputmethod.latin.SuggestedWords;
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import org.futo.inputmethod.latin.WordComposer;
import org.futo.inputmethod.latin.common.Constants;
import org.futo.inputmethod.latin.common.InputPointers;
import org.futo.inputmethod.latin.common.StringUtils;
import org.futo.inputmethod.latin.define.DebugFlags;
import org.futo.inputmethod.latin.settings.Settings;
import org.futo.inputmethod.latin.settings.SettingsValues;
import org.futo.inputmethod.latin.settings.SettingsValuesForSuggestion;
import org.futo.inputmethod.latin.settings.SpacingAndPunctuations;
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
import org.futo.inputmethod.latin.utils.AsyncResultHolder;
import org.futo.inputmethod.latin.utils.InputTypeUtils;
import org.futo.inputmethod.latin.utils.RecapitalizeStatus;
import org.futo.inputmethod.latin.utils.StatsUtils;
import org.futo.inputmethod.latin.utils.TextRange;
import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
/**
* This class manages the input logic.
*/
public final class InputLogic {
private static final String TAG = InputLogic.class.getSimpleName();
// TODO : Remove this member when we can.
Aleksandras Kostarevas
committed
final LatinIMELegacy mLatinIMELegacy;
public final SuggestionStripViewAccessor mSuggestionStripViewAccessor;
// Never null.
private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
// TODO : make all these fields private as soon as possible.
// Current space state of the input method. This can be any of the above constants.
Adrian Velicu
committed
public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
public final Suggest mSuggest;
private final DictionaryFacilitator mDictionaryFacilitator;
public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
public final WordComposer mWordComposer;
public final RichInputConnection mConnection;
private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>();
// Keeps track of most recently inserted text (multi-character key) for reverting
private String mEnteredText;
// TODO: This boolean is persistent state and causes large side effects at unexpected times.
// Find a way to remove it for readability.
private boolean mIsAutoCorrectionIndicatorOn;
private long mDoubleSpacePeriodCountdownStart;
// The word being corrected while the cursor is in the middle of the word.
// Note: This does not have a composing span, so it must be handled separately.
private String mWordBeingCorrectedByCursor = null;
private boolean isLogicSuppressedByExternalLogic = false;
public void startSuppressingLogic() {
isLogicSuppressedByExternalLogic = true;
}
public void endSuppressingLogic() {
isLogicSuppressedByExternalLogic = false;
}
/**
* Create a new instance of the input logic.
Aleksandras Kostarevas
committed
* @param latinIMELegacy the instance of the parent LatinIME. We should remove this when we can.
* @param suggestionStripViewAccessor an object to access the suggestion strip view.
* @param dictionaryFacilitator facilitator for getting suggestions and updating user history
* dictionary.
Aleksandras Kostarevas
committed
public InputLogic(final LatinIMELegacy latinIMELegacy,
final SuggestionStripViewAccessor suggestionStripViewAccessor,
final DictionaryFacilitator dictionaryFacilitator) {
Aleksandras Kostarevas
committed
mLatinIMELegacy = latinIMELegacy;
mSuggestionStripViewAccessor = suggestionStripViewAccessor;
mWordComposer = new WordComposer();
Aleksandras Kostarevas
committed
mConnection = new RichInputConnection(latinIMELegacy.getInputMethodService());
mSuggest = new Suggest(dictionaryFacilitator);
mDictionaryFacilitator = dictionaryFacilitator;
/**
* Initializes the input logic for input in an editor.
*
* Call this when input starts or restarts in some editor (typically, in onStartInputView).
*
* @param combiningSpec the combining spec string for this subtype
* @param settingsValues the current settings values
public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
mEnteredText = null;
mWordBeingCorrectedByCursor = null;
mConnection.onStartInput();
Mohammadinamul Sheik
committed
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();
Adrian Velicu
committed
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();
cancelDoubleSpacePeriodCountdown();
if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) {
Aleksandras Kostarevas
committed
mInputLogicHandler = new InputLogicHandler(mLatinIMELegacy, this);
} else {
mInputLogicHandler.reset();
}
if (settingsValues.mShouldShowLxxSuggestionUi) {
mConnection.requestCursorUpdates(true /* enableMonitor */,
true /* requestImmediateCallback */);
/**
* Call this when the subtype changes.
* @param combiningSpec the spec string for the combining rules
* @param settingsValues the current settings values
public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) {
startInput(combiningSpec, settingsValues);
/**
* Call this when the orientation changes.
* @param settingsValues the current values of the settings.
*/
public void onOrientationChange(final SettingsValues settingsValues) {
// If !isComposingWord, #commitTyped() is a no-op, but still, it's better to avoid
// the useless IPC of {begin,end}BatchEdit.
if (mWordComposer.isComposingWord()) {
mConnection.beginBatchEdit();
// If we had a composition in progress, we need to commit the word so that the
// suggestionsSpan will be added. This will allow resuming on the same suggestions
// after rotation is finished.
commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
mConnection.endBatchEdit();
}
}
/**
* Clean up the input logic after input is finished.
*/
if (mWordComposer.isComposingWord()) {
mConnection.finishComposingText();
StatsUtils.onWordCommitUserTyped(
mWordComposer.getTypedWord(), mWordComposer.isBatchMode());
mIsAutoCorrectionIndicatorOn = false;
resetComposingState(true /* alsoResetLastComposedWord */);
mInputLogicHandler.reset();
// Normally this class just gets out of scope after the process ends, but in unit tests, we
// create several instances of LatinIME in the same process, which results in several
// instances of InputLogic. This cleans up the associated handler so that tests don't leak
// handlers.
public void recycle() {
final InputLogicHandler inputLogicHandler = mInputLogicHandler;
mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
inputLogicHandler.destroy();
mDictionaryFacilitator.closeDictionaries();
/**
* React to a string input.
*
* This is triggered by keys that input many characters at once, like the ".com" key or
* some additional keys for example.
*
* @param settingsValues the current values of the settings.
* @param event the input event containing the data.
* @return the complete transaction object
public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event,
Aleksandras Kostarevas
committed
final int keyboardShiftMode, final LatinIMELegacy.UIHandler handler) {
final String rawText = event.getTextToCommit().toString();
final InputTransaction inputTransaction = new InputTransaction(settingsValues, event,
SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
mConnection.beginBatchEdit();
if (mWordComposer.isComposingWord()) {
commitCurrentAutoCorrection(settingsValues, rawText, handler);
} else {
resetComposingState(true /* alsoResetLastComposedWord */);
}
handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING);
final String text = performSpecificTldProcessingOnTextInput(rawText);
if (SpaceState.PHANTOM == mSpaceState) {
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
}
mConnection.commitText(text, 1);
StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode());
mConnection.endBatchEdit();
// Space state must be updated before calling updateShiftState
mSpaceState = SpaceState.NONE;
mEnteredText = text;
mWordBeingCorrectedByCursor = null;
inputTransaction.setDidAffectContents();
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
return inputTransaction;
/**
* A suggestion was picked from the suggestion strip.
* @param settingsValues the current values of the settings.
* @param suggestionInfo the suggestion info.
* @param keyboardShiftState the shift state of the keyboard, as returned by
* {@link org.futo.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
* @return the complete transaction object
*/
// Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
// interface
public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues,
final SuggestedWordInfo suggestionInfo, final int keyboardShiftState,
Aleksandras Kostarevas
committed
final int currentKeyboardScriptId, final LatinIMELegacy.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);
return onCodeInput(settingsValues, event, keyboardShiftState,
currentKeyboardScriptId, handler);
final Event event = Event.createSuggestionPickedEvent(suggestionInfo);
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState);
// Manual pick affects the contents of the editor, so we take note of this. It's important
// for the sequence of language switching.
inputTransaction.setDidAffectContents();
if(suggestionInfo.mKindAndFlags == SuggestedWordInfo.KIND_UNDO) {
inputTransaction.setRequiresUpdateSuggestions();
mConnection.finishComposingText();
mWordComposer.reset();
mConnection.commitText(suggestionInfo.mWord, 1);
return inputTransaction;
}
mConnection.beginBatchEdit();
if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0
// In the batch input mode, a manually picked suggested word should just replace
// the current batch input text and there is no need for a phantom space.
&& !mWordComposer.isBatchMode()) {
final int firstChar = Character.codePointAt(suggestion, 0);
if (!settingsValues.isWordSeparator(firstChar)
|| settingsValues.isUsuallyPrecededBySpace(firstChar)) {
Aleksandras Kostarevas
committed
if(!mConnection.spacePrecedesComposingText())
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
// TODO: We should not need the following branch. We should be able to take the same
// code path as for other kinds, use commitChosenWord, and do everything normally. We will
// 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)) {
Adrian Velicu
committed
mSuggestedWords = SuggestedWords.getEmptyInstance();
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
resetComposingState(true /* alsoResetLastComposedWord */);
mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo);
commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
LastComposedWord.NOT_A_SEPARATOR, suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED) ? 3 : 1);
mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
// Space state must be updated before calling updateShiftState
Aleksandras Kostarevas
committed
insertOrSetPhantomSpace(settingsValues);
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
// 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());
/**
* Consider an update to the cursor position. Evaluate whether this update has happened as
* part of normal typing or whether it was an explicit cursor move by the user. In any case,
* do the necessary adjustments.
* @param oldSelStart old selection start
* @param oldSelEnd old selection end
* @param newSelStart new selection start
* @param newSelEnd new selection end
* @param settingsValues the current values of the settings.
* @return whether the cursor has moved as a result of user interaction.
*/
public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd,
final int newSelStart, final int newSelEnd, final SettingsValues settingsValues) {
if(isLogicSuppressedByExternalLogic) return false;
if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) {
return false;
}
// TODO: the following is probably better done in resetEntireInputState().
// it should only happen when the cursor moved, and the very purpose of the
// test below is to narrow down whether this happened or not. Likewise with
// the call to updateShiftState.
// We set this to NONE because after a cursor move, we don't want the space
// state-related special processing to kick in.
mSpaceState = SpaceState.NONE;
final boolean selectionChangedOrSafeToReset =
oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed
|| !mWordComposer.isComposingWord(); // safe to reset
final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd);
final int moveAmount = newSelStart - oldSelStart;
// As an added small gift from the framework, it happens upon rotation when there
// is a selection that we get a wrong cursor position delivered to startInput() that
// does not get reflected in the oldSel{Start,End} parameters to the next call to
// onUpdateSelection. In this case, we may have set a composition, and when we're here
// we realize we shouldn't have. In theory, in this case, selectionChangedOrSafeToReset
// should be true, but that is if the framework had taken that wrong cursor position
// into account, which means we have to reset the entire composing state whenever there
// is or was a selection regardless of whether it changed or not.
if (hasOrHadSelection || !settingsValues.needsToLookupSuggestions()
|| (selectionChangedOrSafeToReset
&& !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
// If we are composing a word and moving the cursor, we would want to set a
// suggestion span for recorrection to work correctly. Unfortunately, that
// would involve the keyboard committing some new text, which would move the
// cursor back to where it was. Latin IME could then fix the position of the cursor
// again, but the asynchronous nature of the calls results in this wreaking havoc
// with selection on double tap and the like.
// Another option would be to send suggestions each time we set the composing
// text, but that is probably too expensive to do, so we decided to leave things
// as is.
// Also, we're posting a resume suggestions message, and this will update the
// suggestions strip in a few milliseconds, so if we cleared the suggestion strip here
// we'd have the suggestion strip noticeably janky. To avoid that, we don't clear
// it here, which means we'll keep outdated suggestions for a split second but the
// visual result is better.
resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */);
// If the user is in the middle of correcting a word, we should learn it before moving
// the cursor away.
if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) {
final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
System.currentTimeMillis());
performAdditionToUserHistoryDictionary(settingsValues, mWordBeingCorrectedByCursor,
NgramContext.EMPTY_PREV_WORDS_INFO, -1);
} else {
// resetEntireInputState calls resetCachesUponCursorMove, but forcing the
// composition to end. But in all cases where we don't reset the entire input
// state, we still want to tell the rich input connection about the new cursor
// position so that it can update its caches.
mConnection.resetCachesUponCursorMoveAndReturnSuccess(
newSelStart, newSelEnd, false /* shouldFinishComposition */);
// The cursor has been moved : we now accept to perform recapitalization
mRecapitalizeStatus.enable();
// We moved the cursor. If we are touching a word, we need to resume suggestion.
mIsAutoCorrectionIndicatorOn = false;
Aleksandras Kostarevas
committed
mLatinIMELegacy.mHandler.postResumeSuggestions(true /* shouldDelay */);
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
mWordBeingCorrectedByCursor = null;
/**
* React to a code input. It may be a code point to insert, or a symbolic value that influences
* the keyboard behavior.
*
* Typically, this is called whenever a key is pressed on the software keyboard. This is not
* the entry point for gesture input; see the onBatchInput* family of functions for this.
*
* @param settingsValues the current settings values.
* @param keyboardShiftMode the current shift mode of the keyboard, as returned by
* {@link org.futo.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
* @return the complete transaction object
public InputTransaction onCodeInput(final SettingsValues settingsValues,
@Nonnull final Event event, final int keyboardShiftMode,
Aleksandras Kostarevas
committed
final int currentKeyboardScriptId, final LatinIMELegacy.UIHandler handler) {
mWordBeingCorrectedByCursor = null;
final Event processedEvent = mWordComposer.processEvent(event);
final InputTransaction inputTransaction = new InputTransaction(settingsValues,
processedEvent, SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
|| inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
Aleksandras Kostarevas
committed
mLastKeyTime = inputTransaction.mTimestamp;
mConnection.beginBatchEdit();
if (!mWordComposer.isComposingWord()) {
// TODO: is this useful? It doesn't look like it should be done here, but rather after
// a word is committed.
mIsAutoCorrectionIndicatorOn = false;
}
// TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
cancelDoubleSpacePeriodCountdown();
Event currentEvent = processedEvent;
while (null != currentEvent) {
if (currentEvent.isConsumed()) {
handleConsumedEvent(currentEvent, inputTransaction);
} else if (currentEvent.isFunctionalKeyEvent()) {
handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId,
handler);
} else {
handleNonFunctionalEvent(currentEvent, inputTransaction, handler);
}
currentEvent = currentEvent.mNextEvent;
// Try to record the word being corrected when the user enters a word character or
// the backspace key.
if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord()
&& (settingsValues.isWordCodePoint(processedEvent.mCodePoint) ||
processedEvent.mKeyCode == Constants.CODE_DELETE)) {
mWordBeingCorrectedByCursor = getWordAtCursor(
settingsValues, currentKeyboardScriptId);
}
if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
&& processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
&& processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
mEnteredText = null;
}
mConnection.endBatchEdit();
updateBoostedCodePoints(
settingsValues,
settingsValues.isWordCodePoint(processedEvent.mCodePoint)
);
return inputTransaction;
/**
* Updates keys whose hitboxes are boosted. This works by looking at the word being composed,
* and checking for next letters that would still produce a valid word within the dictionary
*/
private void updateBoostedCodePoints(
final SettingsValues settingsValues,
final boolean wasWordCodePoint
) {
final KeyDetector keyDetector = mLatinIMELegacy.mKeyboardSwitcher.getMainKeyboardView().mKeyDetector;
// Require key boosting setting to be enabled
if(settingsValues.mUseDictionaryKeyBoosting
// text field must allow autocorrection
&& settingsValues.mAutoCorrectionEnabledPerUserSettings
// previous codepoint must have been a word codepoint (i.e. exclude boosting after backspace or symbols)
&& wasWordCodePoint
// accessibility must not be enabled
&& !AccessibilityUtils.getInstance().isAccessibilityEnabled()
) {
keyDetector.updateBoostedCodePoints(mSuggest.getValidNextCodePoints(mWordComposer));
} else {
keyDetector.updateBoostedCodePoints(null);
}
}
public void showBatchSuggestions(final SuggestedWords suggestedWordsForBatchInput,
final boolean isTailBatchInput) {
mInputLogicHandler.showGestureSuggestionsWithPreviewVisuals(suggestedWordsForBatchInput, isTailBatchInput);
}
public void onStartBatchInput(final SettingsValues settingsValues,
Aleksandras Kostarevas
committed
final KeyboardSwitcher keyboardSwitcher, final LatinIMELegacy.UIHandler handler) {
mWordBeingCorrectedByCursor = null;
mInputLogicHandler.onStartBatchInput();
Adrian Velicu
committed
SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */);
handler.cancelUpdateSuggestionStrip();
++mAutoCommitSequenceNumber;
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.
// We also need to unlearn the original word that is now being corrected.
unlearnWord(mWordComposer.getTypedWord(), settingsValues,
Constants.EVENT_BACKSPACE);
resetEntireInputState(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
// We auto-correct the previous (typed, not gestured) string iff it's one character
// long. The reason for this is, even in the middle of gesture typing, you'll still
// tap one-letter words and you want them auto-corrected (typically, "i" in English
// should become "I"). However for any longer word, we assume that the reason for
// tapping probably is that the word you intend to type is not in the dictionary,
// so we do not attempt to correct, on the assumption that if that was a dictionary
// word, the user would probably have gestured instead.
commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR,
} else {
commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
}
}
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
if (Character.isLetterOrDigit(codePointBeforeCursor)
|| settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() !=
getCurrentAutoCapsState(settingsValues);
mSpaceState = SpaceState.PHANTOM;
if (!autoShiftHasBeenOverriden) {
// When we change the space state, we need to update the shift state of the
// keyboard unless it has been overridden manually. This is happening for example
// after typing some letters and a period, then gesturing; the keyboard is not in
// caps mode yet, but since a gesture is starting, it should go in caps mode,
// unless the user explictly said it should not.
keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues),
getCurrentRecapitalizeState());
}
}
mConnection.endBatchEdit();
mWordComposer.setCapitalizedModeAtStartComposingTime(
getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()));
}
/* The sequence number member is only used in onUpdateBatchInput. It is increased each time
* auto-commit happens. The reason we need this is, when auto-commit happens we trim the
* input pointers that are held in a singleton, and to know how much to trim we rely on the
* results of the suggestion process that is held in mSuggestedWords.
* However, the suggestion process is asynchronous, and sometimes we may enter the
* onUpdateBatchInput method twice without having recomputed suggestions yet, or having
* received new suggestions generated from not-yet-trimmed input pointers. In this case, the
* mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
* remove an unrelated number of pointers (possibly even more than are left in the input
* pointers, leading to a crash).
* To avoid that, we increase the sequence number each time we auto-commit and trim the
* input pointers, and we do not use any suggested words that have been generated with an
* earlier sequence number.
*/
private int mAutoCommitSequenceNumber = 1;
public void onUpdateBatchInput(final InputPointers batchPointers) {
mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
public void onEndBatchInput(final InputPointers batchPointers) {
mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber);
++mAutoCommitSequenceNumber;
Aleksandras Kostarevas
committed
public void onCancelBatchInput(final LatinIMELegacy.UIHandler handler) {
mInputLogicHandler.onCancelBatchInput();
Adrian Velicu
committed
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) {
Adrian Velicu
committed
if (!suggestedWords.isEmpty()) {
suggestedWords.mWillAutoCorrect = suggestedWords.mWillAutoCorrect
&& !mConnection.textBeforeCursorLooksLikeURL();
final SuggestedWordInfo suggestedWordInfo;
if (suggestedWords.mWillAutoCorrect) {
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.
suggestedWordInfo = suggestedWords.mTypedWordInfo;
mWordComposer.setAutoCorrection(suggestedWordInfo);
mSuggestedWords = suggestedWords;
final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;
// Put a blue underline to a word in TextView which will be auto-corrected.
if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
&& mWordComposer.isComposingWord()) {
mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
final CharSequence textWithUnderline =
getTextWithUnderline(mWordComposer.getTypedWord());
// TODO: when called from an updateSuggestionStrip() call that results from a posted
// message, this is called outside any batch edit. Potentially, this may result in some
// janky flickering of the screen, although the display speed makes it unlikely in
// the practice.
setComposingTextInternal(textWithUnderline, 1);
/**
* Handle a consumed event.
*
* Consumed events represent events that have already been consumed, typically by the
* combining chain.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) {
// A consumed event may have text to commit and an update to the composing state, so
// we evaluate both. With some combiners, it's possible than an event contains both
// and we enter both of the following if clauses.
final CharSequence textToCommit = event.getTextToCommit();
if (!TextUtils.isEmpty(textToCommit)) {
mConnection.commitText(textToCommit, 1);
inputTransaction.setDidAffectContents();
}
if (mWordComposer.isComposingWord()) {
setComposingTextInternal(mWordComposer.getTypedWord(), 1);
inputTransaction.setDidAffectContents();
inputTransaction.setRequiresUpdateSuggestions();
}
}
/**
* Handle a functional key event.
*
* A functional event is a special key, like delete, shift, emoji, or the settings key.
* Non-special keys are those that generate a single code point.
* This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
* any key that results in multiple code points like the ".com" key.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
Aleksandras Kostarevas
committed
final int currentKeyboardScriptId, final LatinIMELegacy.UIHandler handler) {
if(event.mKeyCode <= Constants.CODE_ACTION_MAX && event.mKeyCode >= Constants.CODE_ACTION_0) {
final int actionId = event.mKeyCode - Constants.CODE_ACTION_0;
handler.triggerAction(actionId);
return;
}
if(event.mKeyCode <= Constants.CODE_ALT_ACTION_MAX && event.mKeyCode >= Constants.CODE_ALT_ACTION_0) {
final int actionId = event.mKeyCode - Constants.CODE_ALT_ACTION_0;
handler.triggerActionAlt(actionId);
return;
}
switch (event.mKeyCode) {
case Constants.CODE_DELETE:
handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId);
// Backspace is a functional key, but it affects the contents of the editor.
inputTransaction.setDidAffectContents();
break;
case Constants.CODE_SHIFT:
performRecapitalization(inputTransaction.mSettingsValues);
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
if (mSuggestedWords.isPrediction()) {
inputTransaction.setRequiresUpdateSuggestions();
}
break;
case Constants.CODE_CAPSLOCK:
// Note: Changing keyboard to shift lock state is handled in
// {@link KeyboardSwitcher#onEvent(Event)}.
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
break;
case Constants.CODE_SYMBOL_SHIFT:
// Note: Calling back to the keyboard on the symbol Shift key is handled in
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
// Note: Calling back to the keyboard on symbol key is handled in
// {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SETTINGS:
onSettingsKeyPressed();
break;
case Constants.CODE_SHORTCUT:
// We need to switch to the shortcut IME. This is handled by LatinIME since the
// input logic has no business with IME switching.
break;
case Constants.CODE_ACTION_NEXT:
performEditorAction(EditorInfo.IME_ACTION_NEXT);
break;
case Constants.CODE_ACTION_PREVIOUS:
performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
break;
case Constants.CODE_LANGUAGE_SWITCH:
handleLanguageSwitchKey();
break;
case Constants.CODE_EMOJI:
// Note: Switching emoji keyboard is being handled in
// {@link KeyboardState#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#onEvent(Event,int)}.
break;
case Constants.CODE_SHIFT_ENTER:
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
// Shift + Enter is treated as a functional key but it results in adding a new
// line, so that does affect the contents of the editor.
inputTransaction.setDidAffectContents();
break;
case Constants.CODE_TO_NUMBER_LAYOUT:
case Constants.CODE_TO_ALT_0_LAYOUT:
case Constants.CODE_TO_ALT_1_LAYOUT:
case Constants.CODE_TO_ALT_2_LAYOUT:
// Handled in KeyboardState
break;
default:
throw new RuntimeException("Unknown key code : " + event.mKeyCode);
}
}
/**
* Handle an event that is not a functional event.
*
* These events are generally events that cause input, but in some cases they may do other
* things like trigger an editor action.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
*/
private void handleNonFunctionalEvent(final Event event,
final InputTransaction inputTransaction,
Aleksandras Kostarevas
committed
final LatinIMELegacy.UIHandler handler) {
inputTransaction.setDidAffectContents();
switch (event.mCodePoint) {
case Constants.CODE_ENTER:
final EditorInfo editorInfo = getCurrentInputEditorInfo();
final int imeOptionsActionId =
InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
// Either we have an actionLabel and we should performEditorAction with
// actionId regardless of its value.
performEditorAction(editorInfo.actionId);
} else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
// We didn't have an actionLabel, but we had another action to execute.
// EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
// EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
// means there should be an action and the app didn't bother to set a specific
// code for it - presumably it only handles one. It does not have to be treated
// in any specific way: anything that is not IME_ACTION_NONE should be sent to
// performEditorAction.
performEditorAction(imeOptionsActionId);
} else {
// No action label, and the action from imeOptions is NONE: this is a regular
// enter key that should input a carriage return.
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
/**
* Handle inputting a code point to the editor.
*
* Non-special keys are those that generate a single code point.
* This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
* any key that results in multiple code points like the ".com" key.
*
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
private void handleNonSpecialCharacterEvent(final Event event,
final InputTransaction inputTransaction,
Aleksandras Kostarevas
committed
final LatinIMELegacy.UIHandler handler) {
final int codePoint = event.mCodePoint;
mSpaceState = SpaceState.NONE;
Aleksandras Kostarevas
committed
if(codePoint == Constants.CODE_SPACE
&& inputTransaction.mSpaceState == SpaceState.ANTIPHANTOM) {
return;
}
if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
|| Character.getType(codePoint) == Character.OTHER_SYMBOL) {
handleSeparatorEvent(event, inputTransaction, handler);
if (SpaceState.PHANTOM == inputTransaction.mSpaceState) {
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can insert the character at the current cursor position.
// We also need to unlearn the original word that is now being corrected.
unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues,
Constants.EVENT_BACKSPACE);
resetEntireInputState(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR);
handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction);
/**
* Handle a non-separator.
* @param event The event to handle.
* @param settingsValues The current settings values.
* @param inputTransaction The transaction in progress.
private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues,
final int codePoint = event.mCodePoint;
// TODO: refactor this method to stop flipping isComposingWord around all the time, and
// make it shorter (possibly cut into several pieces). Also factor
// handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
// not the same.
boolean isComposingWord = mWordComposer.isComposingWord();
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
// See onStartBatchInput() to see how to do it.
if (SpaceState.PHANTOM == inputTransaction.mSpaceState
&& !settingsValues.isWordConnector(codePoint)) {
// Validity check
throw new RuntimeException("Should not be composing here");
}
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
}
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can insert the character at the current cursor position.
// We also need to unlearn the original word that is now being corrected.
unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues,
Constants.EVENT_BACKSPACE);
resetEntireInputState(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
isComposingWord = false;
}
// We want to find out whether to start composing a new word with this character. If so,
// we need to reset the composing state and switch isComposingWord. The order of the
// tests is important for good performance.
// We only start composing if we're not already composing.
if (!isComposingWord
// We only start composing if this is a word code point. Essentially that means it's a
// a letter or a word connector.
&& settingsValues.isWordCodePoint(codePoint)
// We never go into composing state if suggestions are not requested.
&& settingsValues.needsToLookupSuggestions() &&
// 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.
// NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it
// can incur a very expensive getTextAfterCursor() lookup, potentially making the
// keyboard UI slow and non-responsive.
// TODO: Cache the text after the cursor so we don't need to go to the InputConnection
// each time. We are already doing this for getTextBeforeCursor().
(!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
|| !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations,
!mConnection.hasSlowInputConnection() /* checkTextAfter */))) {
// Reset entirely the composing state anyway, then start composing a new word unless
// the character is a word connector. The idea here is, word connectors 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 = !settingsValues.mSpacingAndPunctuations.isWordConnector(codePoint);
// Here we don't need to reset the last composed word. It will be reset
// when we commit this one, if we ever do; if on the other hand we backspace
// it entirely and resume suggestions on the previous word, we'd like to still
// have touch coordinates for it.
resetComposingState(false /* alsoResetLastComposedWord */);
}
if (isComposingWord) {
mWordComposer.applyProcessedEvent(event);
// If it's the first letter, make note of auto-caps state
mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
mSpaceState = SpaceState.WEAK;
boolean autoInsertSpaces = canInsertAutoSpace(settingsValues)
&& (settingsValues.mAltSpacesMode >= Settings.SPACES_MODE_ALL)
&& !mConnection.digitPrecedesCursor();
boolean spacePrecedesCursor = mConnection.spacePrecedesCursor();
sendKeyCodePoint(settingsValues, codePoint);
if(autoInsertSpaces
&& spacePrecedesCursor
&& settingsValues.isUsuallyFollowedBySpaceIffPrecededBySpace(codePoint)) {
insertOrSetPhantomSpace(settingsValues);
}
inputTransaction.setRequiresUpdateSuggestions();
/**
* Handle input of a separator code point.
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction,
Aleksandras Kostarevas
committed
final LatinIMELegacy.UIHandler handler) {
final int codePoint = event.mCodePoint;
final SettingsValues settingsValues = inputTransaction.mSettingsValues;
final boolean wasComposingWord = mWordComposer.isComposingWord();
// We avoid sending spaces in languages without spaces if we were composing.
final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
&& !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
&& wasComposingWord;
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {