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
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 9f5a722..064db6a 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.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 @@
     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;
@@ -77,6 +84,12 @@
             "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
      * keep this to compare it to the actual selection start to guess whether the move was caused by
@@ -110,6 +123,11 @@
     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 @@
         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 @@
         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 @@
             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 @@
                 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 @@
                         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 87c3ddb..03c4ec6 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 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 @@
         }
         // 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 @@
                 && 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 @@
                 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 @@
 
     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 @@
         // 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 @@
             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);