diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/RichInputConnection.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/RichInputConnection.java | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java new file mode 100644 index 000000000..21441369e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2012 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; + +import android.inputmethodservice.InputMethodService; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; + +import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.research.ResearchLogger; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Enrichment class for InputConnection to simplify interaction and add functionality. + * + * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying + * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC + * all the time to find out what text is in the buffer, when we need it to determine caps mode + * for example. + */ +public final class RichInputConnection { + private static final String TAG = RichInputConnection.class.getSimpleName(); + private static final boolean DBG = false; + private static final boolean DEBUG_PREVIOUS_TEXT = false; + private static final boolean DEBUG_BATCH_NESTING = false; + // Provision for a long word pair and a separator + private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1; + private static final Pattern spaceRegex = Pattern.compile("\\s+"); + private static final int INVALID_CURSOR_POSITION = -1; + + /** + * This variable contains the value LatinIME thinks the cursor position should be at now. + * This is a few steps in advance of what the TextView thinks it is, because TextView will + * only know after the IPC calls gets through. + */ + private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points + /** + * This contains the committed text immediately preceding the cursor and the composing + * text if any. It is refreshed when the cursor moves by calling upon the TextView. + */ + private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); + /** + * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. + */ + private StringBuilder mComposingText = new StringBuilder(); + /** + * This is a one-character string containing the character after the cursor. Since LatinIME + * never touches it directly, it's never modified by any means other than re-reading from the + * TextView when the cursor position is changed by the user. + */ + private CharSequence mCharAfterTheCursor = ""; + // A hint on how many characters to cache from the TextView. A good value of this is given by + // how many characters we need to be able to almost always find the caps mode. + private static final int DEFAULT_TEXT_CACHE_SIZE = 100; + + private final InputMethodService mParent; + InputConnection mIC; + int mNestLevel; + public RichInputConnection(final InputMethodService parent) { + mParent = parent; + mIC = null; + mNestLevel = 0; + } + + private void checkConsistencyForDebug() { + final ExtractedTextRequest r = new ExtractedTextRequest(); + r.hintMaxChars = 0; + r.hintMaxLines = 0; + r.token = 1; + r.flags = 0; + final ExtractedText et = mIC.getExtractedText(r, 0); + final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0); + final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText) + .append(mComposingText); + if (null == et || null == beforeCursor) return; + final int actualLength = Math.min(beforeCursor.length(), internal.length()); + if (internal.length() > actualLength) { + internal.delete(0, internal.length() - actualLength); + } + final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() + : beforeCursor.subSequence(beforeCursor.length() - actualLength, + beforeCursor.length()).toString(); + if (et.selectionStart != mCurrentCursorPosition + || !(reference.equals(internal.toString()))) { + final String context = "Expected cursor position = " + mCurrentCursorPosition + + "\nActual cursor position = " + et.selectionStart + + "\nExpected text = " + internal.length() + " " + internal + + "\nActual text = " + reference.length() + " " + reference; + ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); + } else { + Log.e(TAG, Utils.getStackTrace(2)); + Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart); + } + } + + public void beginBatchEdit() { + if (++mNestLevel == 1) { + mIC = mParent.getCurrentInputConnection(); + if (null != mIC) { + mIC.beginBatchEdit(); + } + } else { + if (DBG) { + throw new RuntimeException("Nest level too deep"); + } else { + Log.e(TAG, "Nest level too deep : " + mNestLevel); + } + } + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void endBatchEdit() { + if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead + if (--mNestLevel == 0 && null != mIC) { + mIC.endBatchEdit(); + } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void resetCachesUponCursorMove(final int newCursorPosition) { + mCurrentCursorPosition = newCursorPosition; + mComposingText.setLength(0); + mCommittedTextBeforeComposingText.setLength(0); + mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); + mCharAfterTheCursor = getTextAfterCursor(1, 0); + if (null != mIC) { + mIC.finishComposingText(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } + } + + private void checkBatchEdit() { + if (mNestLevel != 1) { + // TODO: exception instead + Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); + Log.e(TAG, Utils.getStackTrace(4)); + } + } + + public void finishComposingText() { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCommittedTextBeforeComposingText.append(mComposingText); + mCurrentCursorPosition += mComposingText.length(); + mComposingText.setLength(0); + if (null != mIC) { + mIC.finishComposingText(); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } + } + + public void commitText(final CharSequence text, final int i) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); + if (null != mIC) { + mIC.commitText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitText(text, i); + } + } + } + + /** + * Gets the caps modes we should be in after this specific string. + * + * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. + * This method also supports faking an additional space after the string passed in argument, + * to support cases where a space will be added automatically, like in phantom space + * state for example. + * Note that for English, we are using American typography rules (which are not specific to + * American English, it's just the most common set of rules for English). + * + * @param inputType a mask of the caps modes to test for. + * @param locale what language should be considered. + * @param hasSpaceBefore if we should consider there should be a space after the string. + * @return the caps modes that should be on as a set of bits + */ + public int getCursorCapsMode(final int inputType, final Locale locale, + final boolean hasSpaceBefore) { + mIC = mParent.getCurrentInputConnection(); + if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; + if (!TextUtils.isEmpty(mComposingText)) { + if (hasSpaceBefore) { + // If we have some composing text and a space before, then we should have + // MODE_CHARACTERS and MODE_WORDS on. + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType; + } else { + // We have some composing text - we should be in MODE_CHARACTERS only. + return TextUtils.CAP_MODE_CHARACTERS & inputType; + } + } + // TODO: this will generally work, but there may be cases where the buffer contains SOME + // information but not enough to determine the caps mode accurately. This may happen after + // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. + // getCapsMode should be updated to be able to return a "not enough info" result so that + // we can get more context only when needed. + if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) { + mCommittedTextBeforeComposingText.append( + getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); + } + // This never calls InputConnection#getCapsMode - in fact, it's a static method that + // never blocks or initiates IPC. + return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale, + hasSpaceBefore); + } + + public int getCodePointBeforeCursor() { + if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE; + return Character.codePointBefore(mCommittedTextBeforeComposingText, + mCommittedTextBeforeComposingText.length()); + } + + public CharSequence getTextBeforeCursor(final int i, final int j) { + // TODO: use mCommittedTextBeforeComposingText if possible to improve performance + mIC = mParent.getCurrentInputConnection(); + if (null != mIC) return mIC.getTextBeforeCursor(i, j); + return null; + } + + public CharSequence getTextAfterCursor(final int i, final int j) { + mIC = mParent.getCurrentInputConnection(); + if (null != mIC) return mIC.getTextAfterCursor(i, j); + return null; + } + + public void deleteSurroundingText(final int i, final int j) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + final int remainingChars = mComposingText.length() - i; + if (remainingChars >= 0) { + mComposingText.setLength(remainingChars); + } else { + mComposingText.setLength(0); + // Never cut under 0 + final int len = Math.max(mCommittedTextBeforeComposingText.length() + + remainingChars, 0); + mCommittedTextBeforeComposingText.setLength(len); + } + if (mCurrentCursorPosition > i) { + mCurrentCursorPosition -= i; + } else { + mCurrentCursorPosition = 0; + } + if (null != mIC) { + mIC.deleteSurroundingText(i, j); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_deleteSurroundingText(i, j); + } + } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void performEditorAction(final int actionId) { + mIC = mParent.getCurrentInputConnection(); + if (null != mIC) { + mIC.performEditorAction(actionId); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_performEditorAction(actionId); + } + } + } + + public void sendKeyEvent(final KeyEvent keyEvent) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // This method is only called for enter or backspace when speaking to old + // applications (target SDK <= 15), or for digits. + // When talking to new applications we never use this method because it's inherently + // racy and has unpredictable results, but for backward compatibility we continue + // sending the key events for only Enter and Backspace because some applications + // mistakenly catch them to do some stuff. + switch (keyEvent.getKeyCode()) { + case KeyEvent.KEYCODE_ENTER: + mCommittedTextBeforeComposingText.append("\n"); + mCurrentCursorPosition += 1; + break; + case KeyEvent.KEYCODE_DEL: + if (0 == mComposingText.length()) { + if (mCommittedTextBeforeComposingText.length() > 0) { + mCommittedTextBeforeComposingText.delete( + mCommittedTextBeforeComposingText.length() - 1, + mCommittedTextBeforeComposingText.length()); + } + } else { + mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); + } + if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1; + break; + case KeyEvent.KEYCODE_UNKNOWN: + if (null != keyEvent.getCharacters()) { + mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); + mCurrentCursorPosition += keyEvent.getCharacters().length(); + } + break; + default: + final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length(); + break; + } + } + if (null != mIC) { + mIC.sendKeyEvent(keyEvent); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_sendKeyEvent(keyEvent); + } + } + } + + public void setComposingText(final CharSequence text, final int i) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); + mComposingText.append(text); + // TODO: support values of i != 1. At this time, this is never called with i != 1. + if (null != mIC) { + mIC.setComposingText(text, i); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setComposingText(text, i); + } + } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void setSelection(final int from, final int to) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + if (null != mIC) { + mIC.setSelection(from, to); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_setSelection(from, to); + } + } + mCurrentCursorPosition = from; + mCommittedTextBeforeComposingText.setLength(0); + mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0)); + } + + public void commitCorrection(final CorrectionInfo correctionInfo) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // This has no effect on the text field and does not change its content. It only makes + // TextView flash the text for a second based on indices contained in the argument. + if (null != mIC) { + mIC.commitCorrection(correctionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCorrection(correctionInfo); + } + } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public void commitCompletion(final CompletionInfo completionInfo) { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + final CharSequence text = completionInfo.getText(); + mCommittedTextBeforeComposingText.append(text); + mCurrentCursorPosition += text.length() - mComposingText.length(); + mComposingText.setLength(0); + if (null != mIC) { + mIC.commitCompletion(completionInfo); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.richInputConnection_commitCompletion(completionInfo); + } + } + if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + } + + public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) { + mIC = mParent.getCurrentInputConnection(); + if (null == mIC) return null; + final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); + if (DEBUG_PREVIOUS_TEXT && null != prev) { + final int checkLength = LOOKBACK_CHARACTER_NUM - 1; + final String reference = prev.length() <= checkLength ? prev.toString() + : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); + final StringBuilder internal = new StringBuilder() + .append(mCommittedTextBeforeComposingText).append(mComposingText); + if (internal.length() > checkLength) { + internal.delete(0, internal.length() - checkLength); + if (!(reference.equals(internal.toString()))) { + final String context = + "Expected text = " + internal + "\nActual text = " + reference; + ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); + } + } + } + return getNthPreviousWord(prev, sentenceSeperators, n); + } + + /** + * Represents a range of text, relative to the current cursor position. + */ + public static final class Range { + /** Characters before selection start */ + public final int mCharsBefore; + + /** + * Characters after selection start, including one trailing word + * separator. + */ + public final int mCharsAfter; + + /** The actual characters that make up a word */ + public final String mWord; + + public Range(int charsBefore, int charsAfter, String word) { + if (charsBefore < 0 || charsAfter < 0) { + throw new IndexOutOfBoundsException(); + } + this.mCharsBefore = charsBefore; + this.mCharsAfter = charsAfter; + this.mWord = word; + } + } + + private static boolean isSeparator(int code, String sep) { + return sep.indexOf(code) != -1; + } + + // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor, + // n = 2 retrieves the word before that, and so on. This splits on whitespace only. + // Also, it won't return words that end in a separator (if the nth word before the cursor + // ends in a separator, it returns null). + // Example : + // (n = 1) "abc def|" -> def + // (n = 1) "abc def |" -> def + // (n = 1) "abc def. |" -> null + // (n = 1) "abc def . |" -> null + // (n = 2) "abc def|" -> abc + // (n = 2) "abc def |" -> abc + // (n = 2) "abc def. |" -> abc + // (n = 2) "abc def . |" -> def + // (n = 2) "abc|" -> null + // (n = 2) "abc |" -> null + // (n = 2) "abc. def|" -> null + public static CharSequence getNthPreviousWord(final CharSequence prev, + final String sentenceSeperators, final int n) { + if (prev == null) return null; + String[] w = spaceRegex.split(prev); + + // If we can't find n words, or we found an empty word, return null. + if (w.length < n || w[w.length - n].length() <= 0) return null; + + // If ends in a separator, return null + char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1); + if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; + + return w[w.length - n]; + } + + /** + * @param separators characters which may separate words + * @return the word that surrounds the cursor, including up to one trailing + * separator. For example, if the field contains "he|llo world", where | + * represents the cursor, then "hello " will be returned. + */ + public String getWordAtCursor(String separators) { + // getWordRangeAtCursor returns null if the connection is null + Range r = getWordRangeAtCursor(separators, 0); + return (r == null) ? null : r.mWord; + } + + private int getCursorPosition() { + mIC = mParent.getCurrentInputConnection(); + if (null == mIC) return INVALID_CURSOR_POSITION; + final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0); + if (extracted == null) { + return INVALID_CURSOR_POSITION; + } + return extracted.startOffset + extracted.selectionStart; + } + + /** + * Returns the text surrounding the cursor. + * + * @param sep a string of characters that split words. + * @param additionalPrecedingWordsCount the number of words before the current word that should + * be included in the returned range + * @return a range containing the text surrounding the cursor + */ + public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) { + mIC = mParent.getCurrentInputConnection(); + if (mIC == null || sep == null) { + return null; + } + CharSequence before = mIC.getTextBeforeCursor(1000, 0); + CharSequence after = mIC.getTextAfterCursor(1000, 0); + if (before == null || after == null) { + return null; + } + + // Going backward, alternate skipping non-separators and separators until enough words + // have been read. + int start = before.length(); + boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at + while (true) { // see comments below for why this is guaranteed to halt + while (start > 0) { + final int codePoint = Character.codePointBefore(before, start); + if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { + break; // inner loop + } + --start; + if (Character.isSupplementaryCodePoint(codePoint)) { + --start; + } + } + // isStoppingAtWhitespace is true every other time through the loop, + // so additionalPrecedingWordsCount is guaranteed to become < 0, which + // guarantees outer loop termination + if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) { + break; // outer loop + } + isStoppingAtWhitespace = !isStoppingAtWhitespace; + } + + // Find last word separator after the cursor + int end = -1; + while (++end < after.length()) { + final int codePoint = Character.codePointAt(after, end); + if (isSeparator(codePoint, sep)) { + break; + } + if (Character.isSupplementaryCodePoint(codePoint)) { + ++end; + } + } + + int cursor = getCursorPosition(); + if (start >= 0 && cursor + end <= after.length() + before.length()) { + String word = before.toString().substring(start, before.length()) + + after.toString().substring(0, end); + return new Range(before.length() - start, end, word); + } + + return null; + } + + public boolean isCursorTouchingWord(final SettingsValues settingsValues) { + CharSequence before = getTextBeforeCursor(1, 0); + CharSequence after = getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0)) + && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) { + return true; + } + if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) + && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) { + return true; + } + return false; + } + + public void removeTrailingSpace() { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + final CharSequence lastOne = getTextBeforeCursor(1, 0); + if (lastOne != null && lastOne.length() == 1 + && lastOne.charAt(0) == Keyboard.CODE_SPACE) { + deleteSurroundingText(1, 0); + } + } + + public boolean sameAsTextBeforeCursor(final CharSequence text) { + final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); + return TextUtils.equals(text, beforeText); + } + + /* (non-javadoc) + * Returns the word before the cursor if the cursor is at the end of a word, null otherwise + */ + public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { + // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, + // separator or end of line/text) + // Example: "test|"<EOL> "te|st" get rejected here + final CharSequence textAfterCursor = getTextAfterCursor(1, 0); + if (!TextUtils.isEmpty(textAfterCursor) + && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; + + // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) + // Example: " -|" gets rejected here but "e-|" and "e|" are okay + CharSequence word = getWordAtCursor(settings.mWordSeparators); + // We don't suggest on leading single quotes, so we have to remove them from the word if + // it starts with single quotes. + while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) { + word = word.subSequence(1, word.length()); + } + if (TextUtils.isEmpty(word)) return null; + // Find the last code point of the string + final int lastCodePoint = Character.codePointBefore(word, word.length()); + // If for some reason the text field contains non-unicode binary data, or if the + // charsequence is exactly one char long and the contents is a low surrogate, return null. + if (!Character.isDefined(lastCodePoint)) return null; + // Bail out if the cursor is not at the end of a word (cursor must be preceded by + // non-whitespace, non-separator, non-start-of-text) + // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. + if (settings.isWordSeparator(lastCodePoint)) return null; + final char firstChar = word.charAt(0); // we just tested that word is not empty + if (word.length() == 1 && !Character.isLetter(firstChar)) return null; + + // We only suggest on words that start with a letter or a symbol that is excluded from + // word separators (see #handleCharacterWhileInBatchEdit). + if (!(Character.isLetter(firstChar) + || settings.isSymbolExcludedFromWordSeparators(firstChar))) { + return null; + } + + return word; + } + + public boolean revertDoubleSpace() { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + // Here we test whether we indeed have a period and a space before us. This should not + // be needed, but it's there just in case something went wrong. + final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); + if (!". ".equals(textBeforeCursor)) { + // Theoretically we should not be coming here if there isn't ". " before the + // cursor, but the application may be changing the text while we are typing, so + // anything goes. We should not crash. + Log.d(TAG, "Tried to revert double-space combo but we didn't find " + + "\". \" just before the cursor."); + return false; + } + deleteSurroundingText(2, 0); + commitText(" ", 1); + return true; + } + + public boolean revertSwapPunctuation() { + if (DEBUG_BATCH_NESTING) checkBatchEdit(); + // Here we test whether we indeed have a space and something else before us. This should not + // be needed, but it's there just in case something went wrong. + final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); + // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to + // enter surrogate pairs this code will have been removed. + if (TextUtils.isEmpty(textBeforeCursor) + || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) { + // We may only come here if the application is changing the text while we are typing. + // This is quite a broken case, but not logically impossible, so we shouldn't crash, + // but some debugging log may be in order. + Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " + + "find a space just before the cursor."); + return false; + } + deleteSurroundingText(2, 0); + commitText(" " + textBeforeCursor.subSequence(0, 1), 1); + return true; + } + + /** + * Heuristic to determine if this is an expected update of the cursor. + * + * Sometimes updates to the cursor position are late because of their asynchronous nature. + * This method tries to determine if this update is one, based on the values of the cursor + * position in the update, and the currently expected position of the cursor according to + * LatinIME's internal accounting. If this is not a belated expected update, then it should + * mean that the user moved the cursor explicitly. + * This is quite robust, but of course it's not perfect. In particular, it will fail in the + * case we get an update A, the user types in N characters so as to move the cursor to A+N but + * we don't get those, and then the user places the cursor between A and A+N, and we get only + * this update and not the ones in-between. This is almost impossible to achieve even trying + * very very hard. + * + * @param oldSelStart The value of the old cursor position in the update. + * @param newSelStart The value of the new cursor position in the update. + * @return whether this is a belated expected update or not. + */ + public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { + // If this is an update that arrives at our expected position, it's a belated update. + if (newSelStart == mCurrentCursorPosition) return true; + // If this is an update that moves the cursor from our expected position, it must be + // an explicit move. + if (oldSelStart == mCurrentCursorPosition) return false; + // The following returns true if newSelStart is between oldSelStart and + // mCurrentCursorPosition. We assume that if the updated position is between the old + // position and the expected position, then it must be a belated update. + return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; + } +} |