From 912016b69f1c0e26ec58ee9d17c8ac7e5711d70d Mon Sep 17 00:00:00 2001 From: Tom Ouyang Date: Mon, 22 Jun 2015 13:43:32 -0700 Subject: [PATCH] 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 --- .../latin/RichInputConnection.java | 44 ++++++++++++++++--- .../latin/inputlogic/InputLogic.java | 33 +++++++++++--- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 9f5a7223c..064db6a25 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -46,6 +46,8 @@ import com.android.inputmethod.latin.utils.SpannableStringUtils; import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.TextRange; +import java.util.concurrent.TimeUnit; + import javax.annotation.Nonnull; 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_AFTER_CURSOR = 40; 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_AFTER_CURSOR = 1; 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", "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 * 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; int mNestLevel; + /** + * The timestamp of the last slow InputConnection operation + */ + private long mLastSlowInputConnectionTime = 0; + public RichInputConnection(final InputMethodService parent) { mParent = parent; mIC = null; @@ -120,6 +138,20 @@ public final class RichInputConnection implements PrivateCommandPerformer { 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() { final ExtractedTextRequest r = new ExtractedTextRequest(); r.hintMaxChars = 0; @@ -395,7 +427,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { if (!isConnected()) { return null; } - long startTime = SystemClock.uptimeMillis(); + final long startTime = SystemClock.uptimeMillis(); final CharSequence result = mIC.getTextBeforeCursor(n, flags); detectLaggyConnection(operation, startTime); return result; @@ -424,6 +456,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { final String operationName = OPERATION_NAMES[operation]; Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); StatsUtils.onInputConnectionLaggy(operation, duration); + mLastSlowInputConnectionTime = SystemClock.uptimeMillis(); } } @@ -666,7 +699,7 @@ public final class RichInputConnection implements PrivateCommandPerformer { OPERATION_GET_WORD_RANGE_AT_CURSOR, NUM_CHARS_TO_GET_BEFORE_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); - final CharSequence after = getTextBeforeCursorAndDetectLaggyConnection( + final CharSequence after = getTextAfterCursorAndDetectLaggyConnection( OPERATION_GET_WORD_RANGE_AT_CURSOR, NUM_CHARS_TO_GET_AFTER_CURSOR, InputConnection.GET_TEXT_WITH_STYLES); @@ -711,8 +744,9 @@ public final class RichInputConnection implements PrivateCommandPerformer { hasUrlSpans); } - public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { - if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations, + boolean checkTextAfter) { + if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) { // If what's after the cursor is a word character, then we're touching a word. return true; } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 87c3ddbc8..03c4ec62f 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -139,6 +139,7 @@ public final class InputLogic { public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; mWordBeingCorrectedByCursor = null; + mConnection.onStartInput(); if (!mWordComposer.getTypedWord().isEmpty()) { // 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. @@ -472,7 +473,7 @@ public final class InputLogic { } // Try to record the word being corrected when the user enters a word character or // the backspace key. - if (!mWordComposer.isComposingWord() + if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord() && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || processedEvent.mKeyCode == Constants.CODE_DELETE)) { mWordBeingCorrectedByCursor = getWordAtCursor( @@ -832,8 +833,14 @@ public final class InputLogic { && settingsValues.needsToLookupSuggestions() && // 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. - (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) - || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { + // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it + // 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 // 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 @@ -1169,7 +1176,9 @@ public final class InputLogic { unlearnWordBeingDeleted( inputTransaction.mSettingsValues, currentKeyboardScriptId); } - if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() + if (mConnection.hasSlowInputConnection()) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() && inputTransaction.mSettingsValues.mSpacingAndPunctuations .mCurrentLanguageHasSpaces && !mConnection.isCursorFollowedByWordCharacter( @@ -1196,6 +1205,13 @@ public final class InputLogic { boolean unlearnWordBeingDeleted( 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 // entered the composing state yet), unlearn the word. // 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 // expect to receive non-words. 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; final boolean wasAutoCapitalized = @@ -1514,7 +1536,8 @@ public final class InputLogic { return; } final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); - if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, + true /* checkTextAfter */)) { // Show predictions. mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION);