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: I383fab0516d3f3a8e0f71e5d760a8336a7730f7cmain
parent
73aaf68337
commit
912016b69f
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue