Detect cases where rotation messes with initialization

...and do a best effort to fix it.

Bug: 10323080
Bug: 10252066
Change-Id: Icb3c9fe85005406bdfce0b7bb143ba0a910a0ddb
main
Jean Chalard 2013-09-20 18:01:32 +09:00
parent 3de1aca289
commit f1d8aa46f9
3 changed files with 129 additions and 17 deletions

View File

@ -138,6 +138,9 @@ public final class Constants {
public static final int SPELL_CHECKER_COORDINATE = -3;
public static final int EXTERNAL_KEYBOARD_COORDINATE = -4;
// 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.
public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024;
// Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
public static final int DICTIONARY_MAX_WORD_LENGTH = 48;

View File

@ -233,6 +233,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private static final int MSG_RESUME_SUGGESTIONS = 4;
private static final int MSG_REOPEN_DICTIONARIES = 5;
private static final int MSG_ON_END_BATCH_INPUT = 6;
private static final int MSG_RESET_CACHES = 7;
private static final int ARG1_NOT_GESTURE_INPUT = 0;
private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
@ -297,6 +298,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case MSG_ON_END_BATCH_INPUT:
latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj);
break;
case MSG_RESET_CACHES:
latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */,
msg.arg2 /* remainingTries */);
break;
}
}
@ -313,6 +318,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
}
public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
removeMessages(MSG_RESET_CACHES);
sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
remainingTries, null));
}
public void cancelUpdateSuggestionStrip() {
removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
@ -852,7 +863,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// span, so we should reset our state unconditionally, even if restarting is true.
mEnteredText = null;
resetComposingState(true /* alsoResetLastComposedWord */);
if (isDifferentTextField) mHandler.postResumeSuggestions();
mDeleteCount = 0;
mSpaceState = SPACE_STATE_NONE;
mRecapitalizeStatus.deactivate();
@ -871,8 +881,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mSuggestedWords = SuggestedWords.EMPTY;
mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
false /* shouldFinishComposition */);
// Sometimes, while rotating, for some reason the framework tells the app we are not
// connected to it and that means we can't refresh the cache. In this case, schedule a
// refresh later.
if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
false /* shouldFinishComposition */)) {
// We try resetting the caches up to 5 times before giving up.
mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
} else {
if (isDifferentTextField) mHandler.postResumeSuggestions();
}
if (isDifferentTextField) {
mainKeyboardView.closing();
@ -899,6 +917,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mLastSelectionStart = editorInfo.initialSelStart;
mLastSelectionEnd = editorInfo.initialSelEnd;
// 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.
tryFixLyingCursorPosition();
mHandler.cancelUpdateSuggestionStrip();
mHandler.cancelDoubleSpacePeriodTimer();
@ -918,6 +939,35 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
/**
* 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.
*/
private void tryFixLyingCursorPosition() {
final CharSequence textBeforeCursor =
mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
if (null == textBeforeCursor) {
mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION;
} else {
final int textLength = textBeforeCursor.length();
if (textLength > mLastSelectionStart
|| (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
&& mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
mLastSelectionStart = textLength;
// We can't figure out the value of mLastSelectionEnd :(
// But at least if it's smaller than mLastSelectionStart something is wrong
if (mLastSelectionStart > mLastSelectionEnd) {
mLastSelectionEnd = mLastSelectionStart;
}
}
}
}
// Initialization of personalization debug settings. This must be called inside
// onStartInputView.
private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) {
@ -1072,7 +1122,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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.resetCachesUponCursorMove(newSelStart,
mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart,
false /* shouldFinishComposition */);
}
@ -1308,7 +1358,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
} else {
setSuggestedWords(settingsValues.mSuggestPuncList, false);
}
mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
shouldFinishComposition);
}
private void resetComposingState(final boolean alsoResetLastComposedWord) {
@ -2852,6 +2903,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHandler.postUpdateSuggestionStrip();
}
/**
* Retry resetting caches in the rich input connection.
*
* When the editor can't be accessed we can't reset the caches, so we schedule a retry.
* This method handles the retry, and re-schedules a new retry if we still can't access.
* We only retry up to 5 times before giving up.
*
* @param tryResumeSuggestions Whether we should resume suggestions or not.
* @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 (0 < remainingTries) {
mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
}
return;
}
tryFixLyingCursorPosition();
if (tryResumeSuggestions) mHandler.postResumeSuggestions();
}
private void revertCommit() {
final String previousWord = mLastComposedWord.mPrevWord;
final String originallyTypedWord = mLastComposedWord.mTypedWord;

View File

@ -73,9 +73,6 @@ public final class RichInputConnection {
* This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
*/
private final StringBuilder mComposingText = new StringBuilder();
// 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;
@ -93,7 +90,8 @@ public final class RichInputConnection {
r.token = 1;
r.flags = 0;
final ExtractedText et = mIC.getExtractedText(r, 0);
final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
0);
final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
.append(mComposingText);
if (null == et || null == beforeCursor) return;
@ -142,19 +140,56 @@ public final class RichInputConnection {
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
public void resetCachesUponCursorMove(final int newCursorPosition,
/**
* Reset the cached text and retrieve it again from the editor.
*
* This should be called when the cursor moved. It's possible that we can't connect to
* the application when doing this; notably, this happens sometimes during rotation, probably
* because of a race condition in the framework. In this case, we just can't retrieve the
* 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.
* @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;
mComposingText.setLength(0);
mCommittedTextBeforeComposingText.setLength(0);
final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor);
mIC = mParent.getCurrentInputConnection();
// Call upon the inputconnection directly since our own method is using the cache, and
// we want to refresh it.
final CharSequence textBeforeCursor = null == mIC ? null :
mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
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");
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;
}
private void checkBatchEdit() {
@ -233,7 +268,8 @@ public final class RichInputConnection {
// 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(DEFAULT_TEXT_CACHE_SIZE, 0);
final CharSequence textBeforeCursor = getTextBeforeCursor(
Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
if (!TextUtils.isEmpty(textBeforeCursor)) {
mCommittedTextBeforeComposingText.append(textBeforeCursor);
}
@ -364,7 +400,7 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
final CharSequence textBeforeCursor =
getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0);
getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0);
mCommittedTextBeforeComposingText.setLength(0);
if (!TextUtils.isEmpty(textBeforeCursor)) {
final int indexOfStartOfComposingText =
@ -406,7 +442,8 @@ public final class RichInputConnection {
}
mExpectedCursorPosition = start;
mCommittedTextBeforeComposingText.setLength(0);
mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
mCommittedTextBeforeComposingText.append(
getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0));
}
public void commitCorrection(final CorrectionInfo correctionInfo) {
@ -525,9 +562,9 @@ public final class RichInputConnection {
if (mIC == null || sep == null) {
return null;
}
final CharSequence before = mIC.getTextBeforeCursor(1000,
final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
InputConnection.GET_TEXT_WITH_STYLES);
final CharSequence after = mIC.getTextAfterCursor(1000,
final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
InputConnection.GET_TEXT_WITH_STYLES);
if (before == null || after == null) {
return null;