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.
*/
package com.android.inputmethod.latin.inputlogic;
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 com.android.inputmethod.compat.SuggestionSpanUtils;
import com.android.inputmethod.event.Event;
import com.android.inputmethod.event.InputTransaction;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryFacilitator;
import com.android.inputmethod.latin.LastComposedWord;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.NgramContext;
import com.android.inputmethod.latin.RichInputConnection;
import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer;
import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.common.InputPointers;
import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.settings.SettingsValues;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
Tadashi G. Takaoka
committed
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
import com.android.inputmethod.latin.utils.AsyncResultHolder;
import com.android.inputmethod.latin.utils.InputTypeUtils;
import com.android.inputmethod.latin.utils.RecapitalizeStatus;
import com.android.inputmethod.latin.utils.StatsUtils;
import com.android.inputmethod.latin.utils.TextRange;
import java.util.ArrayList;
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.
private 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;
// This has package visibility so it can be accessed from InputLogicHandler.
/* package */ 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;
/**
* Create a new instance of the input logic.
* @param latinIME 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.
public InputLogic(final LatinIME latinIME,
final SuggestionStripViewAccessor suggestionStripViewAccessor,
final DictionaryFacilitator dictionaryFacilitator) {
mSuggestionStripViewAccessor = suggestionStripViewAccessor;
mWordComposer = new WordComposer();
mConnection = new RichInputConnection(latinIME);
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;
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) {
mInputLogicHandler = new InputLogicHandler(mLatinIME, 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());
}
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,
final int keyboardShiftMode,
// TODO: remove this argument
final LatinIME.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;
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 com.android.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,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
final SuggestedWords suggestedWords = mSuggestedWords;
final String suggestion = suggestionInfo.mWord;
// If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) {
// We still want to log a suggestion click.
StatsUtils.onPickSuggestionManually(
mSuggestedWords, suggestionInfo, mDictionaryFacilitator);
// Word separators are suggested before the user inputs something.
// Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo);
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();
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)) {
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);
mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
// Space state must be updated before calling updateShiftState
mSpaceState = SpaceState.PHANTOM;
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 (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 */);
} 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.
mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */);
// Stop the last recapitalization, if started.
mRecapitalizeStatus.stop();
/**
* 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 com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()}
* @return the complete transaction object
public InputTransaction onCodeInput(final SettingsValues settingsValues,
@Nonnull final Event event, final int keyboardShiftMode,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
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) {
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;
if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
&& processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
&& processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
mEnteredText = null;
}
mConnection.endBatchEdit();
return inputTransaction;
public void onStartBatchInput(final SettingsValues settingsValues,
// TODO: remove these arguments
final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
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.
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 SettingsValues settingsValues,
final InputPointers batchPointers,
// TODO: remove these arguments
final KeyboardSwitcher keyboardSwitcher) {
mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
public void onEndBatchInput(final InputPointers batchPointers) {
mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber);
++mAutoCommitSequenceNumber;
// TODO: remove this argument
public void onCancelBatchInput(final LatinIME.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()) {
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,
// TODO: remove these arguments
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
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)}.
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
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:
// TODO: remove this object
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;
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,
// TODO: remove this argument
final LatinIME.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,
final int codePoint = event.mCodePoint;
mSpaceState = SpaceState.NONE;
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.
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)) {
if (isComposingWord) {
// Sanity 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.
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.
(!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)
|| !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) {
// 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;
} else {
sendKeyCodePoint(settingsValues, codePoint);
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,
// TODO: remove this argument
final LatinIME.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()) {
// 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(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
}
// isComposingWord() may have changed since we stored wasComposing
if (mWordComposer.isComposingWord()) {
if (settingsValues.mAutoCorrectionEnabledPerUserSettings) {
final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
: StringUtils.newSingleCodePointString(codePoint);
commitCurrentAutoCorrection(settingsValues, separator, handler);
StringUtils.newSingleCodePointString(codePoint));
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
&& mConnection.isInsideDoubleQuoteOrAfterDigit();
final boolean needsPrecedingSpace;
if (SpaceState.PHANTOM != inputTransaction.mSpaceState) {
needsPrecedingSpace = false;
} else if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
// Double quotes behave like they are usually preceded by space iff we are
// not inside a double quote or after a digit.
needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit;
} else if (settingsValues.mSpacingAndPunctuations.isClusteringSymbol(codePoint)
&& settingsValues.mSpacingAndPunctuations.isClusteringSymbol(
mConnection.getCodePointBeforeCursor())) {
needsPrecedingSpace = false;
needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint);
}
if (needsPrecedingSpace) {
insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues);
if (tryPerformDoubleSpacePeriod(event, inputTransaction)) {
mSpaceState = SpaceState.DOUBLE;
inputTransaction.setRequiresUpdateSuggestions();
StatsUtils.onDoubleSpacePeriod();
} else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
mSpaceState = SpaceState.SWAP_PUNCTUATION;
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
} else if (Constants.CODE_SPACE == codePoint) {
if (!mSuggestedWords.isPunctuationSuggestions()) {
startDoubleSpacePeriodCountdown(inputTransaction);
if (wasComposingWord || mSuggestedWords.isEmpty()) {
inputTransaction.setRequiresUpdateSuggestions();
}
if (!shouldAvoidSendingCode) {
sendKeyCodePoint(settingsValues, codePoint);
}
if ((SpaceState.PHANTOM == inputTransaction.mSpaceState
&& settingsValues.isUsuallyFollowedBySpace(codePoint))
|| (Constants.CODE_DOUBLE_QUOTE == codePoint
&& isInsideDoubleQuoteOrAfterDigit)) {
// If we are in phantom space state, and the user presses a separator, we want to
// stay in phantom space state so that the next keypress has a chance to add the
// space. For example, if I type "Good dat", pick "day" from the suggestion strip
// then insert a comma and go on to typing the next word, I want the space to be
// inserted automatically before the next word, the same way it is when I don't
// input the comma. A double quote behaves like it's usually followed by space if
// we're inside a double quote.
// The case is a little different if the separator is a space stripper. Such a
// separator does not normally need a space on the right (that's the difference
// between swappers and strippers), so we should not stay in phantom space state if
// the separator is a stripper. Hence the additional test above.
mSpaceState = SpaceState.PHANTOM;
}
sendKeyCodePoint(settingsValues, codePoint);
// Set punctuation right away. onUpdateSelection will fire but tests whether it is
// already displayed or not, so it's okay.
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
/**
* Handle a press on the backspace key.
* @param event The event to handle.
* @param inputTransaction The transaction in progress.
private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction,
// TODO: remove this argument, put it into settingsValues
// In many cases after backspace, we need to update the shift state. Normally we need
// to do this right away to avoid the shift state being out of date in case the user types
// backspace then some other character very fast. However, in the case of backspace key
// repeat, this can lead to flashiness when the cursor flies over positions where the
// shift state should be updated, so if this is a key repeat, we update after a small delay.
// Then again, even in the case of a key repeat, if the cursor is at start of text, it
// can't go any further back, so we can update right away even if it's a key repeat.
final int shiftUpdateKind =
event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0
? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW;
inputTransaction.requireShiftUpdate(shiftUpdateKind);
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can remove the character at the current cursor position.
resetEntireInputState(mConnection.getExpectedSelectionStart(),
mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
// When we exit this if-clause, mWordComposer.isComposingWord() will return false.
}
if (mWordComposer.isComposingWord()) {
if (mWordComposer.isBatchMode()) {
final String rejectedSuggestion = mWordComposer.getTypedWord();
mWordComposer.reset();
mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
if (!TextUtils.isEmpty(rejectedSuggestion)) {
unlearnWord(rejectedSuggestion, inputTransaction.mSettingsValues,
Constants.EVENT_REJECTION);
StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length());
StatsUtils.onBackspacePressed(1);
setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);