Workaround for preserving responsiveness on a slow InputConnection.

1. Add mechanism to detect a slow or non-resonsive InputConnection (IC)
2. When IC slowness is detected, skip certain IC calls that are known
   to be expensive (e.g., getTextAfterCursor).
3. Similarly, disables learning / unlearning on a slow IC.
4. IC slowness flag is reset when starting input on a new TextView or
   when a fixed amount of time has passed.

Note: These are mostly temporary workarounds. The permanent solution is
to refactor RichInputConnection so that it is less sensitive to IC
slowness in general.

Bug: 21926256
Change-Id: I383fab0516d3f3a8e0f71e5d760a8336a7730f7c
main
Tom Ouyang 2015-06-22 13:43:32 -07:00
parent 73aaf68337
commit 912016b69f
2 changed files with 67 additions and 10 deletions

View File

@ -46,6 +46,8 @@ import com.android.inputmethod.latin.utils.SpannableStringUtils;
import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.StatsUtils;
import com.android.inputmethod.latin.utils.TextRange; import com.android.inputmethod.latin.utils.TextRange;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -65,7 +67,12 @@ public final class RichInputConnection implements PrivateCommandPerformer {
private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40; private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40;
private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40; private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40;
private static final int INVALID_CURSOR_POSITION = -1; private static final int INVALID_CURSOR_POSITION = -1;
private static final long SLOW_INPUTCONNECTION_MS = 100;
/**
* The amount of time an InputConnection call needs to take for the keyboard to enter
* the SlowInputConnection state.
*/
private static final long SLOW_INPUTCONNECTION_MS = 200;
private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0; private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0;
private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1; private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1;
private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2; private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2;
@ -76,6 +83,12 @@ public final class RichInputConnection implements PrivateCommandPerformer {
"GET_WORD_RANGE_AT_CURSOR", "GET_WORD_RANGE_AT_CURSOR",
"RELOAD_TEXT_CACHE"}; "RELOAD_TEXT_CACHE"};
/**
* The amount of time the keyboard will persist in the 'hasSlowInputConnection' state
* after observing a slow InputConnection event.
*/
private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10);
/** /**
* This variable contains an expected value for the selection start position. This is where the * 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 * cursor or selection start may end up after all the keyboard-triggered updates have passed. We
@ -110,6 +123,11 @@ public final class RichInputConnection implements PrivateCommandPerformer {
InputConnection mIC; InputConnection mIC;
int mNestLevel; int mNestLevel;
/**
* The timestamp of the last slow InputConnection operation
*/
private long mLastSlowInputConnectionTime = 0;
public RichInputConnection(final InputMethodService parent) { public RichInputConnection(final InputMethodService parent) {
mParent = parent; mParent = parent;
mIC = null; mIC = null;
@ -120,6 +138,20 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return mIC != null; return mIC != null;
} }
/**
* Returns whether or not the underlying InputConnection is slow. When true, we want to avoid
* calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor).
*/
public boolean hasSlowInputConnection() {
return mLastSlowInputConnectionTime > 0 &&
(SystemClock.uptimeMillis() - mLastSlowInputConnectionTime)
<= SLOW_INPUTCONNECTION_PERSIST_MS;
}
public void onStartInput() {
mLastSlowInputConnectionTime = 0;
}
private void checkConsistencyForDebug() { private void checkConsistencyForDebug() {
final ExtractedTextRequest r = new ExtractedTextRequest(); final ExtractedTextRequest r = new ExtractedTextRequest();
r.hintMaxChars = 0; r.hintMaxChars = 0;
@ -395,7 +427,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (!isConnected()) { if (!isConnected()) {
return null; return null;
} }
long startTime = SystemClock.uptimeMillis(); final long startTime = SystemClock.uptimeMillis();
final CharSequence result = mIC.getTextBeforeCursor(n, flags); final CharSequence result = mIC.getTextBeforeCursor(n, flags);
detectLaggyConnection(operation, startTime); detectLaggyConnection(operation, startTime);
return result; return result;
@ -424,6 +456,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
final String operationName = OPERATION_NAMES[operation]; final String operationName = OPERATION_NAMES[operation];
Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms.");
StatsUtils.onInputConnectionLaggy(operation, duration); StatsUtils.onInputConnectionLaggy(operation, duration);
mLastSlowInputConnectionTime = SystemClock.uptimeMillis();
} }
} }
@ -666,7 +699,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
OPERATION_GET_WORD_RANGE_AT_CURSOR, OPERATION_GET_WORD_RANGE_AT_CURSOR,
NUM_CHARS_TO_GET_BEFORE_CURSOR, NUM_CHARS_TO_GET_BEFORE_CURSOR,
InputConnection.GET_TEXT_WITH_STYLES); InputConnection.GET_TEXT_WITH_STYLES);
final CharSequence after = getTextBeforeCursorAndDetectLaggyConnection( final CharSequence after = getTextAfterCursorAndDetectLaggyConnection(
OPERATION_GET_WORD_RANGE_AT_CURSOR, OPERATION_GET_WORD_RANGE_AT_CURSOR,
NUM_CHARS_TO_GET_AFTER_CURSOR, NUM_CHARS_TO_GET_AFTER_CURSOR,
InputConnection.GET_TEXT_WITH_STYLES); InputConnection.GET_TEXT_WITH_STYLES);
@ -711,8 +744,9 @@ public final class RichInputConnection implements PrivateCommandPerformer {
hasUrlSpans); hasUrlSpans);
} }
public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { boolean checkTextAfter) {
if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
// If what's after the cursor is a word character, then we're touching a word. // If what's after the cursor is a word character, then we're touching a word.
return true; return true;
} }

View File

@ -139,6 +139,7 @@ public final class InputLogic {
public void startInput(final String combiningSpec, final SettingsValues settingsValues) { public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
mEnteredText = null; mEnteredText = null;
mWordBeingCorrectedByCursor = null; mWordBeingCorrectedByCursor = null;
mConnection.onStartInput();
if (!mWordComposer.getTypedWord().isEmpty()) { if (!mWordComposer.getTypedWord().isEmpty()) {
// For messaging apps that offer send button, the IME does not get the opportunity // For messaging apps that offer send button, the IME does not get the opportunity
// to capture the last word. This block should capture those uncommitted words. // to capture the last word. This block should capture those uncommitted words.
@ -472,7 +473,7 @@ public final class InputLogic {
} }
// Try to record the word being corrected when the user enters a word character or // Try to record the word being corrected when the user enters a word character or
// the backspace key. // the backspace key.
if (!mWordComposer.isComposingWord() if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord()
&& (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) ||
processedEvent.mKeyCode == Constants.CODE_DELETE)) { processedEvent.mKeyCode == Constants.CODE_DELETE)) {
mWordBeingCorrectedByCursor = getWordAtCursor( mWordBeingCorrectedByCursor = getWordAtCursor(
@ -832,8 +833,14 @@ public final class InputLogic {
&& settingsValues.needsToLookupSuggestions() && && settingsValues.needsToLookupSuggestions() &&
// In languages with spaces, we only start composing a word when we are not already // In languages with spaces, we only start composing a word when we are not already
// touching a word. In languages without spaces, the above conditions are sufficient. // touching a word. In languages without spaces, the above conditions are sufficient.
(!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it
|| !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { // can incur a very expensive getTextAfterCursor() lookup, potentially making the
// keyboard UI slow and non-responsive.
// TODO: Cache the text after the cursor so we don't need to go to the InputConnection
// each time. We are already doing this for getTextBeforeCursor().
(!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
|| !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations,
!mConnection.hasSlowInputConnection() /* checkTextAfter */))) {
// Reset entirely the composing state anyway, then start composing a new word unless // Reset entirely the composing state anyway, then start composing a new word unless
// the character is a word connector. The idea here is, word connectors are not // the character is a word connector. The idea here is, word connectors are not
// separators and they should be treated as normal characters, except in the first // separators and they should be treated as normal characters, except in the first
@ -1169,7 +1176,9 @@ public final class InputLogic {
unlearnWordBeingDeleted( unlearnWordBeingDeleted(
inputTransaction.mSettingsValues, currentKeyboardScriptId); inputTransaction.mSettingsValues, currentKeyboardScriptId);
} }
if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() if (mConnection.hasSlowInputConnection()) {
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
} else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings()
&& inputTransaction.mSettingsValues.mSpacingAndPunctuations && inputTransaction.mSettingsValues.mSpacingAndPunctuations
.mCurrentLanguageHasSpaces .mCurrentLanguageHasSpaces
&& !mConnection.isCursorFollowedByWordCharacter( && !mConnection.isCursorFollowedByWordCharacter(
@ -1196,6 +1205,13 @@ public final class InputLogic {
boolean unlearnWordBeingDeleted( boolean unlearnWordBeingDeleted(
final SettingsValues settingsValues, final int currentKeyboardScriptId) { final SettingsValues settingsValues, final int currentKeyboardScriptId) {
if (mConnection.hasSlowInputConnection()) {
// TODO: Refactor unlearning so that it does not incur any extra calls
// to the InputConnection. That way it can still be performed on a slow
// InputConnection.
Log.w(TAG, "Skipping unlearning due to slow InputConnection.");
return false;
}
// If we just started backspacing to delete a previous word (but have not // If we just started backspacing to delete a previous word (but have not
// entered the composing state yet), unlearn the word. // entered the composing state yet), unlearn the word.
// TODO: Consider tracking whether or not this word was typed by the user. // TODO: Consider tracking whether or not this word was typed by the user.
@ -1411,6 +1427,12 @@ public final class InputLogic {
// That's to avoid unintended additions in some sensitive fields, or fields that // That's to avoid unintended additions in some sensitive fields, or fields that
// expect to receive non-words. // expect to receive non-words.
if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return; if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return;
if (mConnection.hasSlowInputConnection()) {
// Since we don't unlearn when the user backspaces on a slow InputConnection,
// turn off learning to guard against adding typos that the user later deletes.
Log.w(TAG, "Skipping learning due to slow InputConnection.");
return;
}
if (TextUtils.isEmpty(suggestion)) return; if (TextUtils.isEmpty(suggestion)) return;
final boolean wasAutoCapitalized = final boolean wasAutoCapitalized =
@ -1514,7 +1536,8 @@ public final class InputLogic {
return; return;
} }
final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations,
true /* checkTextAfter */)) {
// Show predictions. // Show predictions.
mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF);
mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION);