Track selection end in RichInputConnection

Change-Id: Ie5cffe03b676dcde83896cda139b42f3829eb528
This commit is contained in:
Kurt Partridge 2013-11-05 17:03:33 -08:00
parent 2bf3a77814
commit d564466d30
2 changed files with 78 additions and 43 deletions

View file

@ -906,7 +906,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// refresh later.
final boolean canReachInputConnection;
if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
false /* shouldFinishComposition */)) {
editorInfo.initialSelEnd, false /* shouldFinishComposition */)) {
// We try resetting the caches up to 5 times before giving up.
mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
// mLastSelection{Start,End} are reset later in this method, don't need to do it here
@ -1108,7 +1108,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: revisit this when LatinIME supports hardware keyboards.
// NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
// TODO: find a better way to simulate actual execution.
if (isInputViewShown() && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
if (isInputViewShown() && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart,
oldSelEnd, newSelEnd)) {
// 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
@ -1134,13 +1135,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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.
resetEntireInputState(newSelStart);
resetEntireInputState(newSelStart, newSelEnd);
} else {
// resetEntireInputState calls resetCachesUponCursorMove, but with the second
// argument as true. 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,
// 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 */);
}
@ -1363,7 +1364,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This will reset the whole input state to the starting state. It will clear
// the composing word, reset the last composed word, tell the inputconnection about it.
private void resetEntireInputState(final int newCursorPosition) {
private void resetEntireInputState(final int newSelStart, final int newSelEnd) {
final boolean shouldFinishComposition = mWordComposer.isComposingWord();
resetComposingState(true /* alsoResetLastComposedWord */);
final SettingsValues settingsValues = mSettings.getCurrent();
@ -1372,7 +1373,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} else {
setSuggestedWords(settingsValues.mSuggestPuncList, false);
}
mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
shouldFinishComposition);
}
@ -1715,7 +1716,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(mLastSelectionStart);
resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
} else {
commitTyped(LastComposedWord.NOT_A_SEPARATOR);
}
@ -1783,7 +1784,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(mLastSelectionStart);
resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
} else if (wordComposerSize <= 1) {
// 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
@ -2071,7 +2072,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(mLastSelectionStart);
resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
// When we exit this if-clause, mWordComposer.isComposingWord() will return false.
}
if (mWordComposer.isComposingWord()) {
@ -2239,7 +2240,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(mLastSelectionStart);
resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
isComposingWord = false;
}
// We want to find out whether to start composing a new word with this character. If so,
@ -2351,7 +2352,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(mLastSelectionStart);
resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
}
if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
if (currentSettings.mCorrectionEnabled) {
@ -2983,7 +2984,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
* @param remainingTries How many times we may try again before giving up.
*/
private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart,
mLastSelectionEnd, false)) {
if (0 < remainingTries) {
mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
return;

View file

@ -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 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.
@ -150,15 +155,17 @@ 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 newSelStart The new position of the selection start, 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 newSelStart,
final boolean shouldFinishComposition) {
final int newSelEnd, final boolean shouldFinishComposition) {
mExpectedSelStart = newSelStart;
mExpectedSelEnd = newSelEnd;
mComposingText.setLength(0);
final boolean didReloadTextSuccessfully = reloadTextCache();
if (!didReloadTextSuccessfully) {
@ -169,11 +176,13 @@ public final class RichInputConnection {
if (lengthOfTextBeforeCursor > newSelStart
|| (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
&& newSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
// newSelStart 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 newSelStart says, then we know it was lying. In both cases the length
// is more reliable.
// newSelStart and newSelEnd 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 newSelStart says, then we know it was lying. In both
// cases the length is more reliable. Note that we only have to check newSelStart (not
// newSelEnd) since if newSelEnd is wrong, the newSelStart will be wrong as well.
mExpectedSelStart = lengthOfTextBeforeCursor;
mExpectedSelEnd = lengthOfTextBeforeCursor;
}
if (null != mIC && shouldFinishComposition) {
mIC.finishComposingText();
@ -200,6 +209,7 @@ public final class RichInputConnection {
// 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.
mExpectedSelStart = INVALID_CURSOR_POSITION;
mExpectedSelEnd = INVALID_CURSOR_POSITION;
Log.e(TAG, "Unable to connect to the editor to retrieve text.");
return false;
}
@ -351,8 +361,12 @@ public final class RichInputConnection {
}
if (mExpectedSelStart > beforeLength) {
mExpectedSelStart -= beforeLength;
mExpectedSelEnd -= beforeLength;
} else {
// 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);
@ -387,6 +401,7 @@ public final class RichInputConnection {
case KeyEvent.KEYCODE_ENTER:
mCommittedTextBeforeComposingText.append("\n");
mExpectedSelStart += 1;
mExpectedSelEnd = mExpectedSelStart;
break;
case KeyEvent.KEYCODE_DEL:
if (0 == mComposingText.length()) {
@ -398,18 +413,24 @@ public final class RichInputConnection {
} else {
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
}
if (mExpectedSelStart > 0) mExpectedSelStart -= 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());
mExpectedSelStart += keyEvent.getCharacters().length();
mExpectedSelEnd = mExpectedSelStart;
}
break;
default:
final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
mCommittedTextBeforeComposingText.append(text);
mExpectedSelStart += text.length();
mExpectedSelEnd = mExpectedSelStart;
break;
}
}
@ -444,9 +465,11 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
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) {
@ -782,20 +805,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 == mExpectedSelStart) return true;
// If this is an update that moves the cursor from our expected position, it must be
// an explicit move.
if (oldSelStart == mExpectedSelStart) 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) * (mExpectedSelStart - 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 mExpeectedSelend match the old
// values, and one of newSelStart or newSelEnd is updated to a different value. In this
// case, there is likely something other than the IME that has moved the selection endpoint
// to the new value.
if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
&& (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
// If nether of the above two cases holds, 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;
}
/**