Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 912016b6 authored by Tom Ouyang's avatar Tom Ouyang
Browse files

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
parent 73aaf683
Loading
Loading
Loading
Loading
+39 −5
Original line number Diff line number Diff line
@@ -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;
        }
+28 −5
Original line number Diff line number Diff line
@@ -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);