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.text.SpannableString;
import android.text.TextUtils;
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.KeyboardSwitcher;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryFacilitator;
import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.LastComposedWord;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.PrevWordsInfo;
import com.android.inputmethod.latin.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.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.StringUtils;
import com.android.inputmethod.latin.utils.TextRange;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* This class manages the input logic.
*/
public final class InputLogic {
private static final String TAG = InputLogic.class.getSimpleName();
// TODO : Remove this member when we can.
private final LatinIME mLatinIME;
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.
// Never null
public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
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
// 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
public void startInput(final String combiningSpec) {
mEnteredText = null;
mWordComposer.restartCombining(combiningSpec);
resetComposingState(true /* alsoResetLastComposedWord */);
mDeleteCount = 0;
mSpaceState = SpaceState.NONE;
mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once
mCurrentlyPressedHardwareKeys.clear();
mSuggestedWords = SuggestedWords.EMPTY;
// 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();
}
/**
* Call this when the subtype changes.
* @param combiningSpec the spec string for the combining rules
*/
public void onSubtypeChanged(final String combiningSpec) {
finishInput();
startInput(combiningSpec);
/**
* 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();
}
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.mText.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();
final String text = performSpecificTldProcessingOnTextInput(rawText);
if (SpaceState.PHANTOM == mSpaceState) {
promotePhantomSpace(settingsValues);
}
mConnection.commitText(text, 1);
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()) {
// 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)) {
promotePhantomSpace(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)) {
mSuggestedWords = SuggestedWords.EMPTY;
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
resetComposingState(true /* alsoResetLastComposedWord */);
mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo);
}
// We need to log before we commit, because the word composer will store away the user
// typed word.
final String replacedWord = mWordComposer.getTypedWord();
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);
// We should show the "Touch again to save" hint if the user pressed the first entry
// AND it's in none of our current dictionaries (main, user or otherwise).
final boolean showingAddToDictionaryHint =
(suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED)
|| suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION))
&& !mDictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */);
if (showingAddToDictionaryHint && mDictionaryFacilitator.isUserDictionaryEnabled()) {
mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion);
} else {
// If we're not showing the "Touch again to save", then update the suggestion strip.
handler.postUpdateSuggestionStrip();
}
/**
* 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
* @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) {
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 || (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(false /* shouldIncludeResumedWordInSuggestions */,
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, 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, event,
SystemClock.uptimeMillis(), mSpaceState,
getActualCapsMode(settingsValues, keyboardShiftMode));
if (event.mKeyCode != Constants.CODE_DELETE
|| inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
mLastKeyTime = inputTransaction.mTimestamp;
mConnection.beginBatchEdit();
if (!mWordComposer.isComposingWord()) {
mIsAutoCorrectionIndicatorOn = false;
}
// TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
if (event.mCodePoint != Constants.CODE_SPACE) {
cancelDoubleSpacePeriodCountdown();
}
boolean didAutoCorrect = false;
if (event.isFunctionalKeyEvent()) {
// A special key, like delete, shift, emoji, or the settings key.
switch (event.mKeyCode) {
case Constants.CODE_DELETE:
handleBackspace(inputTransaction, currentKeyboardScriptId, processedEvent);
// 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.mIsPrediction) {
inputTransaction.setRequiresUpdateSuggestions();
}
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
break;
case Constants.CODE_CAPSLOCK:
// Note: Changing keyboard to shift lock state is handled in
// {@link KeyboardSwitcher#onCodeInput(int)}.
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#onCodeInput(int,int)}.
break;
case Constants.CODE_ALPHA_FROM_EMOJI:
// Note: Switching back from Emoji keyboard to the main keyboard is being
// handled in {@link KeyboardState#onCodeInput(int,int)}.
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());
final InputTransaction tmpTransaction = new InputTransaction(
inputTransaction.mSettingsValues, tmpEvent,
inputTransaction.mTimestamp, inputTransaction.mSpaceState,
inputTransaction.mShiftState);
didAutoCorrect = handleNonSpecialCharacter(tmpTransaction, handler, processedEvent);
// 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);
}
} else {
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.
didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler,
processedEvent);
}
break;
default:
didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler,
processedEvent);
if (!didAutoCorrect && event.mKeyCode != Constants.CODE_SHIFT
&& event.mKeyCode != Constants.CODE_CAPSLOCK
&& event.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
if (Constants.CODE_DELETE != event.mKeyCode) {
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();
handler.showGesturePreviewAndSuggestionStrip(
SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
handler.cancelUpdateSuggestionStrip();
++mAutoCommitSequenceNumber;
mConnection.beginBatchEdit();
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) {
if (settingsValues.mPhraseGestureEnabled) {
final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
// If these suggested words have been generated with out of date input pointers, then
// we skip auto-commit (see comments above on the mSequenceNumber member).
if (null != candidate
&& mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
final String[] commitParts = candidate.mWord.split(Constants.WORD_SEPARATOR, 2);
batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
promotePhantomSpace(settingsValues);
mConnection.commitText(commitParts[0], 0);
mSpaceState = SpaceState.PHANTOM;
keyboardSwitcher.requestUpdatingShiftState(
getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode(
settingsValues, keyboardSwitcher.getKeyboardShiftMode()));
++mAutoCommitSequenceNumber;
}
}
}
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();
handler.showGesturePreviewAndSuggestionStrip(
SuggestedWords.EMPTY, 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) {
if (SuggestedWords.EMPTY != suggestedWords) {
final String autoCorrection;
if (suggestedWords.mWillAutoCorrect) {
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 = suggestedWords.mTypedWord;
}
mWordComposer.setAutoCorrection(autoCorrection);
}
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.
mConnection.setComposingText(textWithUnderline, 1);
}
}
/**
* 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 inputTransaction The transaction in progress.
* @return whether this caused an auto-correction to happen.
*/
private boolean handleNonSpecialCharacter(final InputTransaction inputTransaction,
final LatinIME.UIHandler handler,
// TODO: remove this argument, put it inside the transaction
final Event processedEvent) {
final int codePoint = processedEvent.mCodePoint;
mSpaceState = SpaceState.NONE;
final boolean didAutoCorrect;
if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
|| Character.getType(codePoint) == Character.OTHER_SYMBOL) {
didAutoCorrect = handleSeparator(inputTransaction,
processedEvent.isSuggestionStripPress(), handler);
} else {
didAutoCorrect = false;
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);
handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction, processedEvent);
}
return didAutoCorrect;
/**
* Handle a non-separator.
* @param settingsValues The current settings values.
* @param inputTransaction The transaction in progress.
*/
private void handleNonSeparator(final SettingsValues settingsValues,
final InputTransaction inputTransaction,
// TODO: remove this arg, put it into the input transaction
final Event processedEvent) {
final int codePoint = processedEvent.mCodePoint;
// TODO: refactor this method to stop flipping isComposingWord around all the time, and
// make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
// which has the same name as other handle* methods but is not the same.
boolean isComposingWord = mWordComposer.isComposingWord();
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
// See onStartBatchInput() to see how to do it.
if (SpaceState.PHANTOM == inputTransaction.mSpaceState
&& !settingsValues.isWordConnector(codePoint)) {
if (isComposingWord) {
// Sanity check
throw new RuntimeException("Should not be composing here");
}
promotePhantomSpace(settingsValues);
}
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can insert the character at the current cursor position.
resetEntireInputState(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(processedEvent);
// If it's the first letter, make note of auto-caps state
mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
}
mConnection.setComposingText(getTextWithUnderline(
mWordComposer.getTypedWord()), 1);
} else {
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
inputTransaction, processedEvent.isSuggestionStripPress());
if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) {
mSpaceState = SpaceState.WEAK;
} else {
sendKeyCodePoint(settingsValues, codePoint);
}
// In case the "add to dictionary" hint was still displayed.
mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
inputTransaction.setRequiresUpdateSuggestions();
/**
* Handle input of a separator code point.
* @param inputTransaction The transaction in progress.
* @param isFromSuggestionStrip whether this code point comes from the suggestion strip.
* @return whether this caused an auto-correction to happen.
*/
private boolean handleSeparator(final InputTransaction inputTransaction,
final boolean isFromSuggestionStrip,
// TODO: remove this argument
final LatinIME.UIHandler handler) {
final int codePoint = inputTransaction.mEvent.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);
didAutoCorrect = true;
} else {
StringUtils.newSingleCodePointString(codePoint));
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
inputTransaction, isFromSuggestionStrip);
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) {
if (tryPerformDoubleSpacePeriod(inputTransaction)) {
mSpaceState = SpaceState.DOUBLE;
inputTransaction.setRequiresUpdateSuggestions();
} else if (swapWeakSpace && trySwapSwapperAndSpace(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);
return didAutoCorrect;
}
/**
* Handle a press on the backspace key.
* @param inputTransaction The transaction in progress.
private void handleBackspace(final InputTransaction inputTransaction,
// TODO: remove this argument, put it into settingsValues
final int currentKeyboardScriptId,
// TODO: remove this argument, put it into the transaction
final Event processedEvent) {
// 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 =
processedEvent.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)) {
mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion);
}
mWordComposer.applyProcessedEvent(processedEvent);
if (mWordComposer.isComposingWord()) {
mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
} else {
mConnection.commitText("", 1);
}
inputTransaction.setRequiresUpdateSuggestions();
} else {
if (mLastComposedWord.canRevertCommit()) {
return;
}
if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
// Cancel multi-character input: remove the text we just entered.
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
mConnection.deleteSurroundingText(mEnteredText.length(), 0);
mEnteredText = null;
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
// In addition we know that spaceState is false, and that we should not be
// reverting any autocorrect at this point. So we can safely return.
return;
}
if (SpaceState.DOUBLE == inputTransaction.mSpaceState) {
cancelDoubleSpacePeriodCountdown();
if (mConnection.revertDoubleSpacePeriod()) {
// No need to reset mSpaceState, it has already be done (that's why we
// receive it as a parameter)
inputTransaction.setRequiresUpdateSuggestions();
mWordComposer.setCapitalizedModeAtStartComposingTime(
WordComposer.CAPS_MODE_OFF);
} else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
if (mConnection.revertSwapPunctuation()) {
// Likewise
return;
}
}
// No cancelling of commit/double space/swap: we have a regular backspace.
// We should backspace one char and restart suggestion if at the end of a word.
if (mConnection.hasSelection()) {
// If there is a selection, remove it.
final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
- mConnection.getExpectedSelectionStart();
mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
mConnection.getExpectedSelectionEnd());
mConnection.deleteSurroundingText(numCharsDeleted, 0);
} else {
// There is no selection, just delete one character.
if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
// This should never happen.
Log.e(TAG, "Backspace when we don't know the selection position");
}