aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/RichInputConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin/RichInputConnection.java')
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java404
1 files changed, 250 insertions, 154 deletions
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 673d1b4c2..30b20a335 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -27,7 +27,7 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.CapsModeUtils;
import com.android.inputmethod.latin.utils.DebugLogUtils;
import com.android.inputmethod.latin.utils.SpannableStringUtils;
@@ -35,7 +35,7 @@ import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.TextRange;
import com.android.inputmethod.research.ResearchLogger;
-import java.util.Locale;
+import java.util.Arrays;
import java.util.regex.Pattern;
/**
@@ -57,14 +57,19 @@ public final class RichInputConnection {
private static final int INVALID_CURSOR_POSITION = -1;
/**
- * This variable contains an expected value for the cursor position. This is where the
- * cursor may end up after all the keyboard-triggered updates have passed. We keep this to
- * compare it to the actual cursor position to guess whether the move was caused by a
- * keyboard command or not.
- * It's not really the cursor position: the cursor may not be there yet, and it's also expected
- * there be cases where it never actually comes to be there.
+ * This variable contains an expected value for the selection start position. This is where the
+ * cursor or selection start may end up after all the keyboard-triggered updates have passed. We
+ * keep this to compare it to the actual selection start to guess whether the move was caused by
+ * a keyboard command or not.
+ * It's not really the selection start position: the selection start may not be there yet, and
+ * in some cases, it may never arrive there.
*/
- private int mExpectedCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
+ private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points
+ /**
+ * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is
+ * expected. The same caveats as mExpectedSelStart apply.
+ */
+ private int mExpectedSelEnd = 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.
@@ -93,7 +98,7 @@ public final class RichInputConnection {
final ExtractedText et = mIC.getExtractedText(r, 0);
final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
0);
- final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
+ final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText)
.append(mComposingText);
if (null == et || null == beforeCursor) return;
final int actualLength = Math.min(beforeCursor.length(), internal.length());
@@ -103,16 +108,16 @@ public final class RichInputConnection {
final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
: beforeCursor.subSequence(beforeCursor.length() - actualLength,
beforeCursor.length()).toString();
- if (et.selectionStart != mExpectedCursorPosition
+ if (et.selectionStart != mExpectedSelStart
|| !(reference.equals(internal.toString()))) {
- final String context = "Expected cursor position = " + mExpectedCursorPosition
- + "\nActual cursor position = " + et.selectionStart
+ final String context = "Expected selection start = " + mExpectedSelStart
+ + "\nActual selection start = " + et.selectionStart
+ "\nExpected text = " + internal.length() + " " + internal
+ "\nActual text = " + reference.length() + " " + reference;
((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
} else {
Log.e(TAG, DebugLogUtils.getStackTrace(2));
- Log.e(TAG, "Exp <> Actual : " + mExpectedCursorPosition + " <> " + et.selectionStart);
+ Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart);
}
}
@@ -150,16 +155,38 @@ public final class RichInputConnection {
* data, so we empty the cache and note that we don't know the new cursor position, and we
* return false so that the caller knows about this and can retry later.
*
- * @param newCursorPosition The new position of the cursor, as received from the system.
- * @param shouldFinishComposition Whether we should finish the composition in progress.
+ * @param newSelStart the new position of the selection start, as received from the system.
+ * @param newSelEnd the new position of the selection end, as received from the system.
+ * @param shouldFinishComposition whether we should finish the composition in progress.
* @return true if we were able to connect to the editor successfully, false otherwise. When
* this method returns false, the caches could not be correctly refreshed so they were only
* reset: the caller should try again later to return to normal operation.
*/
- public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newCursorPosition,
- final boolean shouldFinishComposition) {
- mExpectedCursorPosition = newCursorPosition;
+ public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart,
+ final int newSelEnd, final boolean shouldFinishComposition) {
+ mExpectedSelStart = newSelStart;
+ mExpectedSelEnd = newSelEnd;
mComposingText.setLength(0);
+ final boolean didReloadTextSuccessfully = reloadTextCache();
+ if (!didReloadTextSuccessfully) {
+ Log.d(TAG, "Will try to retrieve text later.");
+ return false;
+ }
+ if (null != mIC && shouldFinishComposition) {
+ mIC.finishComposingText();
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Reload the cached text from the InputConnection.
+ *
+ * @return true if successful
+ */
+ private boolean reloadTextCache() {
mCommittedTextBeforeComposingText.setLength(0);
mIC = mParent.getCurrentInputConnection();
// Call upon the inputconnection directly since our own method is using the cache, and
@@ -169,27 +196,12 @@ public final class RichInputConnection {
if (null == textBeforeCursor) {
// For some reason the app thinks we are not connected to it. This looks like a
// framework bug... Fall back to ground state and return false.
- mExpectedCursorPosition = INVALID_CURSOR_POSITION;
- Log.e(TAG, "Unable to connect to the editor to retrieve text... will retry later");
+ mExpectedSelStart = INVALID_CURSOR_POSITION;
+ mExpectedSelEnd = INVALID_CURSOR_POSITION;
+ Log.e(TAG, "Unable to connect to the editor to retrieve text.");
return false;
}
mCommittedTextBeforeComposingText.append(textBeforeCursor);
- final int lengthOfTextBeforeCursor = textBeforeCursor.length();
- if (lengthOfTextBeforeCursor > newCursorPosition
- || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
- && newCursorPosition < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
- // newCursorPosition may be lying -- when rotating the device (probably a framework
- // bug). If we have less chars than we asked for, then we know how many chars we have,
- // and if we got more than newCursorPosition says, then we know it was lying. In both
- // cases the length is more reliable
- mExpectedCursorPosition = lengthOfTextBeforeCursor;
- }
- if (null != mIC && shouldFinishComposition) {
- mIC.finishComposingText();
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.richInputConnection_finishComposingText();
- }
- }
return true;
}
@@ -204,6 +216,9 @@ public final class RichInputConnection {
public void finishComposingText() {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ // TODO: this is not correct! The cursor is not necessarily after the composing text.
+ // In the practice right now this is only called when input ends so it will be reset so
+ // it works, but it's wrong and should be fixed.
mCommittedTextBeforeComposingText.append(mComposingText);
mComposingText.setLength(0);
if (null != mIC) {
@@ -218,7 +233,11 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ // TODO: the following is exceedingly error-prone. Right now when the cursor is in the
+ // middle of the composing word mComposingText only holds the part of the composing text
+ // that is before the cursor, so this actually works, but it's terribly confusing. Fix this.
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitText(text, i);
@@ -226,12 +245,11 @@ public final class RichInputConnection {
}
public CharSequence getSelectedText(final int flags) {
- if (null == mIC) return null;
- return mIC.getSelectedText(flags);
+ return (null == mIC) ? null : mIC.getSelectedText(flags);
}
public boolean canDeleteCharacters() {
- return mExpectedCursorPosition > 0;
+ return mExpectedSelStart > 0;
}
/**
@@ -245,12 +263,12 @@ public final class RichInputConnection {
* 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 settingsValues the values of the settings to use for locale and separators.
+ * @param spacingAndPunctuations the values of the settings to use for locale and separators.
* @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 SettingsValues settingsValues,
- final boolean hasSpaceBefore) {
+ public int getCursorCapsMode(final int inputType,
+ final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
mIC = mParent.getCurrentInputConnection();
if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
if (!TextUtils.isEmpty(mComposingText)) {
@@ -268,23 +286,22 @@ public final class RichInputConnection {
// 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 != mExpectedCursorPosition) {
- final CharSequence textBeforeCursor = getTextBeforeCursor(
- Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
- if (!TextUtils.isEmpty(textBeforeCursor)) {
- mCommittedTextBeforeComposingText.append(textBeforeCursor);
+ if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) {
+ if (!reloadTextCache()) {
+ Log.w(TAG, "Unable to connect to the editor. "
+ + "Setting caps mode without knowing text.");
}
}
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
// never blocks or initiates IPC.
return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
- settingsValues, hasSpaceBefore);
+ spacingAndPunctuations, hasSpaceBefore);
}
public int getCodePointBeforeCursor() {
- if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE;
- return Character.codePointBefore(mCommittedTextBeforeComposingText,
- mCommittedTextBeforeComposingText.length());
+ final int length = mCommittedTextBeforeComposingText.length();
+ if (length < 1) return Constants.NOT_A_CODE;
+ return Character.codePointBefore(mCommittedTextBeforeComposingText, length);
}
public CharSequence getTextBeforeCursor(final int n, final int flags) {
@@ -295,8 +312,8 @@ public final class RichInputConnection {
// However, if we don't have an expected cursor position, then we should always
// go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to
// test for this explicitly)
- if (INVALID_CURSOR_POSITION != mExpectedCursorPosition
- && (cachedLength >= n || cachedLength >= mExpectedCursorPosition)) {
+ if (INVALID_CURSOR_POSITION != mExpectedSelStart
+ && (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
// We call #toString() here to create a temporary object.
// In some situations, this method is called on a worker thread, and it's possible
@@ -312,20 +329,19 @@ public final class RichInputConnection {
return s;
}
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) {
- return mIC.getTextBeforeCursor(n, flags);
- }
- return null;
+ return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags);
}
public CharSequence getTextAfterCursor(final int n, final int flags) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) return mIC.getTextAfterCursor(n, flags);
- return null;
+ return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags);
}
public void deleteSurroundingText(final int beforeLength, final int afterLength) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ // TODO: the following is incorrect if the cursor is not immediately after the composition.
+ // Right now we never come here in this case because we reset the composing state before we
+ // come here in this case, but we need to fix this.
final int remainingChars = mComposingText.length() - beforeLength;
if (remainingChars >= 0) {
mComposingText.setLength(remainingChars);
@@ -336,10 +352,14 @@ public final class RichInputConnection {
+ remainingChars, 0);
mCommittedTextBeforeComposingText.setLength(len);
}
- if (mExpectedCursorPosition > beforeLength) {
- mExpectedCursorPosition -= beforeLength;
+ if (mExpectedSelStart > beforeLength) {
+ mExpectedSelStart -= beforeLength;
+ mExpectedSelEnd -= beforeLength;
} else {
- mExpectedCursorPosition = 0;
+ // There are fewer characters before the cursor in the buffer than we are being asked to
+ // delete. Only delete what is there.
+ mExpectedSelStart = 0;
+ mExpectedSelEnd -= mExpectedSelStart;
}
if (null != mIC) {
mIC.deleteSurroundingText(beforeLength, afterLength);
@@ -373,7 +393,8 @@ public final class RichInputConnection {
switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_ENTER:
mCommittedTextBeforeComposingText.append("\n");
- mExpectedCursorPosition += 1;
+ mExpectedSelStart += 1;
+ mExpectedSelEnd = mExpectedSelStart;
break;
case KeyEvent.KEYCODE_DEL:
if (0 == mComposingText.length()) {
@@ -385,18 +406,24 @@ public final class RichInputConnection {
} else {
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
}
- if (mExpectedCursorPosition > 0) mExpectedCursorPosition -= 1;
+ if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) {
+ // TODO: Handle surrogate pairs.
+ mExpectedSelStart -= 1;
+ }
+ mExpectedSelEnd = mExpectedSelStart;
break;
case KeyEvent.KEYCODE_UNKNOWN:
if (null != keyEvent.getCharacters()) {
mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
- mExpectedCursorPosition += keyEvent.getCharacters().length();
+ mExpectedSelStart += keyEvent.getCharacters().length();
+ mExpectedSelEnd = mExpectedSelStart;
}
break;
default:
- final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
+ final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar());
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length();
+ mExpectedSelStart += text.length();
+ mExpectedSelEnd = mExpectedSelStart;
break;
}
}
@@ -415,8 +442,12 @@ public final class RichInputConnection {
getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0);
mCommittedTextBeforeComposingText.setLength(0);
if (!TextUtils.isEmpty(textBeforeCursor)) {
+ // The cursor is not necessarily at the end of the composing text, but we have its
+ // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start
+ // of the text, so we should use mExpectedSelStart. In other words, the composing
+ // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor
final int indexOfStartOfComposingText =
- Math.max(textBeforeCursor.length() - (end - start), 0);
+ Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0);
mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText,
textBeforeCursor.length()));
mCommittedTextBeforeComposingText.append(
@@ -430,10 +461,12 @@ public final class RichInputConnection {
public void setComposingText(final CharSequence text, final int newCursorPosition) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
mComposingText.append(text);
- // TODO: support values of i != 1. At this time, this is never called with i != 1.
+ // TODO: support values of newCursorPosition != 1. At this time, this is never called with
+ // newCursorPosition != 1.
if (null != mIC) {
mIC.setComposingText(text, newCursorPosition);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -443,19 +476,31 @@ public final class RichInputConnection {
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
- public void setSelection(final int start, final int end) {
+ /**
+ * Set the selection of the text editor.
+ *
+ * Calls through to {@link InputConnection#setSelection(int, int)}.
+ *
+ * @param start the character index where the selection should start.
+ * @param end the character index where the selection should end.
+ * @return Returns true on success, false if the input connection is no longer valid either when
+ * setting the selection or when retrieving the text cache at that point.
+ */
+ public boolean setSelection(final int start, final int end) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ mExpectedSelStart = start;
+ mExpectedSelEnd = end;
if (null != mIC) {
- mIC.setSelection(start, end);
+ final boolean isIcValid = mIC.setSelection(start, end);
+ if (!isIcValid) {
+ return false;
+ }
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.richInputConnection_setSelection(start, end);
}
}
- mExpectedCursorPosition = start;
- mCommittedTextBeforeComposingText.setLength(0);
- mCommittedTextBeforeComposingText.append(
- getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0));
+ return reloadTextCache();
}
public void commitCorrection(final CorrectionInfo correctionInfo) {
@@ -476,7 +521,8 @@ public final class RichInputConnection {
// text should never be null, but just in case, it's better to insert nothing than to crash
if (null == text) text = "";
mCommittedTextBeforeComposingText.append(text);
- mExpectedCursorPosition += text.length() - mComposingText.length();
+ mExpectedSelStart += text.length() - mComposingText.length();
+ mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitCompletion(completionInfo);
@@ -488,7 +534,8 @@ public final class RichInputConnection {
}
@SuppressWarnings("unused")
- public String getNthPreviousWord(final String sentenceSeperators, final int n) {
+ public String getNthPreviousWord(final SpacingAndPunctuations spacingAndPunctuations,
+ final int n) {
mIC = mParent.getCurrentInputConnection();
if (null == mIC) return null;
final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
@@ -496,6 +543,9 @@ public final class RichInputConnection {
final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
final String reference = prev.length() <= checkLength ? prev.toString()
: prev.subSequence(prev.length() - checkLength, prev.length()).toString();
+ // TODO: right now the following works because mComposingText holds the part of the
+ // composing text that is before the cursor, but this is very confusing. We should
+ // fix it.
final StringBuilder internal = new StringBuilder()
.append(mCommittedTextBeforeComposingText).append(mComposingText);
if (internal.length() > checkLength) {
@@ -507,11 +557,11 @@ public final class RichInputConnection {
}
}
}
- return getNthPreviousWord(prev, sentenceSeperators, n);
+ return getNthPreviousWord(prev, spacingAndPunctuations, n);
}
- private static boolean isSeparator(int code, String sep) {
- return sep.indexOf(code) != -1;
+ private static boolean isSeparator(final int code, final int[] sortedSeparators) {
+ return Arrays.binarySearch(sortedSeparators, code) >= 0;
}
// Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
@@ -531,7 +581,7 @@ public final class RichInputConnection {
// (n = 2) "abc |" -> null
// (n = 2) "abc. def|" -> null
public static String getNthPreviousWord(final CharSequence prev,
- final String sentenceSeperators, final int n) {
+ final SpacingAndPunctuations spacingAndPunctuations, final int n) {
if (prev == null) return null;
final String[] w = spaceRegex.split(prev);
@@ -543,35 +593,36 @@ public final class RichInputConnection {
// If ends in a separator, return null
final char lastChar = nthPrevWord.charAt(length - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+ if (spacingAndPunctuations.isWordSeparator(lastChar)
+ || spacingAndPunctuations.isWordConnector(lastChar)) return null;
return nthPrevWord;
}
/**
- * @param separators characters which may separate words
+ * @param sortedSeparators a sorted array of code points 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 CharSequence getWordAtCursor(String separators) {
+ public CharSequence getWordAtCursor(final int[] sortedSeparators) {
// getWordRangeAtCursor returns null if the connection is null
- TextRange r = getWordRangeAtCursor(separators, 0);
+ final TextRange r = getWordRangeAtCursor(sortedSeparators, 0);
return (r == null) ? null : r.mWord;
}
/**
* Returns the text surrounding the cursor.
*
- * @param sep a string of characters that split words.
+ * @param sortedSeparators a sorted array of code points 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 TextRange getWordRangeAtCursor(final String sep,
+ public TextRange getWordRangeAtCursor(final int[] sortedSeparators,
final int additionalPrecedingWordsCount) {
mIC = mParent.getCurrentInputConnection();
- if (mIC == null || sep == null) {
+ if (mIC == null) {
return null;
}
final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
@@ -590,7 +641,7 @@ public final class RichInputConnection {
while (true) { // see comments below for why this is guaranteed to halt
while (startIndexInBefore > 0) {
final int codePoint = Character.codePointBefore(before, startIndexInBefore);
- if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+ if (isStoppingAtWhitespace == isSeparator(codePoint, sortedSeparators)) {
break; // inner loop
}
--startIndexInBefore;
@@ -611,7 +662,7 @@ public final class RichInputConnection {
int endIndexInAfter = -1;
while (++endIndexInAfter < after.length()) {
final int codePoint = Character.codePointAt(after, endIndexInAfter);
- if (isSeparator(codePoint, sep)) {
+ if (isSeparator(codePoint, sortedSeparators)) {
break;
}
if (Character.isSupplementaryCodePoint(codePoint)) {
@@ -619,27 +670,40 @@ public final class RichInputConnection {
}
}
+ final boolean hasUrlSpans =
+ SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
+ || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
// We don't use TextUtils#concat because it copies all spans without respect to their
// nature. If the text includes a PARAGRAPH span and it has been split, then
// TextUtils#concat will crash when it tries to concat both sides of it.
return new TextRange(
SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
- startIndexInBefore, before.length() + endIndexInAfter, before.length());
+ startIndexInBefore, before.length() + endIndexInAfter, before.length(),
+ hasUrlSpans);
}
- public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+ public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) {
final int codePointBeforeCursor = getCodePointBeforeCursor();
- if (Constants.NOT_A_CODE != codePointBeforeCursor
- && !settingsValues.isWordSeparator(codePointBeforeCursor)
- && !settingsValues.isWordConnector(codePointBeforeCursor)) {
- return true;
+ if (Constants.NOT_A_CODE == codePointBeforeCursor
+ || spacingAndPunctuations.isWordSeparator(codePointBeforeCursor)
+ || spacingAndPunctuations.isWordConnector(codePointBeforeCursor)) {
+ return isCursorFollowedByWordCharacter(spacingAndPunctuations);
}
+ return true;
+ }
+
+ public boolean isCursorFollowedByWordCharacter(
+ final SpacingAndPunctuations spacingAndPunctuations) {
final CharSequence after = getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
- && !settingsValues.isWordConnector(after.charAt(0))) {
- return true;
+ if (TextUtils.isEmpty(after)) {
+ return false;
}
- return false;
+ final int codePointAfterCursor = Character.codePointAt(after, 0);
+ if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor)
+ || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) {
+ return false;
+ }
+ return true;
}
public void removeTrailingSpace() {
@@ -655,45 +719,6 @@ public final class RichInputConnection {
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) && Constants.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 don't restart suggestion if the first character is not a letter, because we don't
- // start composing when the first character is not a letter.
- if (!Character.isLetter(firstChar)) return null;
-
- return word;
- }
-
public boolean revertDoubleSpacePeriod() {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
// Here we test whether we indeed have a period and a space before us. This should not
@@ -758,20 +783,30 @@ public final class RichInputConnection {
* 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.
+ * @param oldSelStart The value of the old selection in the update.
+ * @param newSelStart The value of the new selection in the update.
+ * @param oldSelEnd The value of the old selection end in the update.
+ * @param newSelEnd The value of the new selection end 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 == mExpectedCursorPosition) return true;
- // If this is an update that moves the cursor from our expected position, it must be
- // an explicit move.
- if (oldSelStart == mExpectedCursorPosition) 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) * (mExpectedCursorPosition - newSelStart) >= 0;
+ public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart,
+ final int oldSelEnd, final int newSelEnd) {
+ // This update is "belated" if we are expecting it. That is, mExpectedSelStart and
+ // mExpectedSelEnd match the new values that the TextView is updating TO.
+ if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true;
+ // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old
+ // values, and one of newSelStart or newSelEnd is updated to a different value. In this
+ // case, it is likely that something other than the IME has moved the selection endpoint
+ // to the new value.
+ if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
+ && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
+ // If neither of the above two cases hold, then the system may be having trouble keeping up
+ // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
+ // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then
+ // assume a belated update.
+ return (newSelStart == newSelEnd)
+ && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0
+ && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0;
}
/**
@@ -784,4 +819,65 @@ public final class RichInputConnection {
public boolean textBeforeCursorLooksLikeURL() {
return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
}
+
+ /**
+ * Looks at the text just before the cursor to find out if we are inside a double quote.
+ *
+ * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached.
+ * However this won't be a concrete problem in most situations, as the cache is almost always
+ * long enough for this use.
+ */
+ public boolean isInsideDoubleQuoteOrAfterDigit() {
+ return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText);
+ }
+
+ /**
+ * Try to get the text from the editor to expose lies the framework may have been
+ * telling us. Concretely, when the device rotates, the frameworks tells us about where the
+ * cursor used to be initially in the editor at the time it first received the focus; this
+ * may be completely different from the place it is upon rotation. Since we don't have any
+ * means to get the real value, try at least to ask the text view for some characters and
+ * detect the most damaging cases: when the cursor position is declared to be much smaller
+ * than it really is.
+ */
+ public void tryFixLyingCursorPosition() {
+ final CharSequence textBeforeCursor = getTextBeforeCursor(
+ Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+ if (null == textBeforeCursor) {
+ mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
+ } else {
+ final int textLength = textBeforeCursor.length();
+ if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
+ && (textLength > mExpectedSelStart
+ || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
+ // It should not be possible to have only one of those variables be
+ // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
+ // (simple cursor, no selection) or there is no cursor/we don't know its pos
+ final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd;
+ mExpectedSelStart = textLength;
+ // We can't figure out the value of mLastSelectionEnd :(
+ // But at least if it's smaller than mLastSelectionStart something is wrong,
+ // and if they used to be equal we also don't want to make it look like there is a
+ // selection.
+ if (wasEqual || mExpectedSelStart > mExpectedSelEnd) {
+ mExpectedSelEnd = mExpectedSelStart;
+ }
+ }
+ }
+ }
+
+ public int getExpectedSelectionStart() {
+ return mExpectedSelStart;
+ }
+
+ public int getExpectedSelectionEnd() {
+ return mExpectedSelEnd;
+ }
+
+ /**
+ * @return whether there is a selection currently active.
+ */
+ public boolean hasSelection() {
+ return mExpectedSelEnd != mExpectedSelStart;
+ }
}