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

Commit 28d765ed authored by Jean Chalard's avatar Jean Chalard
Browse files

Make Latin IME aware of its surrounding text.

This is a preparatory change for
Bug: 4967874
Bug: 6617760
Bug: 6950087

Change-Id: I3abf8e45c0d02c42491421f108370220134b9602
parent 29352761
Loading
Loading
Loading
Loading
+25 −8
Original line number Diff line number Diff line
@@ -733,6 +733,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled,
                mCurrentSettings.mGestureFloatingPreviewTextEnabled);

        mConnection.resetCachesUponCursorMove(mLastSelectionStart);

        if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
    }

@@ -839,7 +841,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
            mSpaceState = SPACE_STATE_NONE;

            if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
                resetEntireInputState();
                resetEntireInputState(newSelStart);
            }

            mHandler.postUpdateShiftState();
@@ -1043,14 +1045,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen

    // This will reset the whole input state to the starting state. It will clear
    // the composing word, reset the last composed word, tell the inputconnection about it.
    private void resetEntireInputState() {
    private void resetEntireInputState(final int newCursorPosition) {
        resetComposingState(true /* alsoResetLastComposedWord */);
        if (mCurrentSettings.mBigramPredictionEnabled) {
            clearSuggestionStrip();
        } else {
            setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
        }
        mConnection.finishComposingText();
        mConnection.resetCachesUponCursorMove(newCursorPosition);
    }

    private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1220,7 +1222,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        }
    }

    private void sendUpDownEnterOrBackspace(final int code) {
    private void sendDownUpKeyEventForBackwardCompatibility(final int code) {
        final long eventTime = SystemClock.uptimeMillis();
        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
                KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
@@ -1234,7 +1236,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        // TODO: Remove this special handling of digit letters.
        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
        if (code >= '0' && code <= '9') {
            super.sendKeyChar((char)code);
            sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.latinIME_sendKeyCodePoint(code);
            }
@@ -1249,7 +1251,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
            // a hardware keyboard event on pressing enter or delete. This is bad for many
            // reasons (there are race conditions with commits) but some applications are
            // relying on this behavior so we continue to support it for older apps.
            sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER);
            sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER);
        } else {
            final String text = new String(new int[] { code }, 0, 1);
            mConnection.commitText(text, text.length());
@@ -1525,7 +1527,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                    // a hardware keyboard event on pressing enter or delete. This is bad for many
                    // reasons (there are race conditions with commits) but some applications are
                    // relying on this behavior so we continue to support it for older apps.
                    sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL);
                    sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
                } else {
                    mConnection.deleteSurroundingText(1, 0);
                }
@@ -1862,7 +1864,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
                    separatorString);
            if (!typedWord.equals(autoCorrection)) {
                // This will make the correction flash for a short while as a visual clue
                // to the user that auto-correction happened.
                // to the user that auto-correction happened. It has no other effect; in particular
                // note that this won't affect the text inside the text field AT ALL: it only makes
                // the segment of text starting at the supplied index and running for the length
                // of the auto-correction flash. At this moment, the "typedWord" argument is
                // ignored by TextView.
                mConnection.commitCorrection(
                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
                        typedWord, autoCorrection));
@@ -2229,6 +2235,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
        dialog.show();
    }

    public void debugDumpStateAndCrashWithException(final String context) {
        final StringBuilder s = new StringBuilder();
        s.append("Target application : ").append(mTargetApplicationInfo.name)
                .append("\nPackage : ").append(mTargetApplicationInfo.packageName)
                .append("\nTarget app sdk version : ")
                .append(mTargetApplicationInfo.targetSdkVersion)
                .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString())
                .append("\nContext : ").append(context);
        throw new RuntimeException(s.toString());
    }

    @Override
    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
        super.dump(fd, fout, args);
+178 −1
Original line number Diff line number Diff line
@@ -33,16 +33,47 @@ import com.android.inputmethod.research.ResearchLogger;
import java.util.regex.Pattern;

/**
 * Wrapper for InputConnection to simplify interaction
 * Enrichment class for InputConnection to simplify interaction and add functionality.
 *
 * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
 * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
 * all the time to find out what text is in the buffer, when we need it to determine caps mode
 * for example.
 */
public class RichInputConnection {
    private static final String TAG = RichInputConnection.class.getSimpleName();
    private static final boolean DBG = false;
    private static final boolean DEBUG_PREVIOUS_TEXT = false;
    // Provision for a long word pair and a separator
    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
    private static final Pattern spaceRegex = Pattern.compile("\\s+");
    private static final int INVALID_CURSOR_POSITION = -1;

    /**
     * This variable contains the value LatinIME thinks the cursor position should be at now.
     * This is a few steps in advance of what the TextView thinks it is, because TextView will
     * only know after the IPC calls gets through.
     */
    private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
    /**
     * This contains the committed text immediately preceding the cursor and the composing
     * text if any. It is refreshed when the cursor moves by calling upon the TextView.
     */
    private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
    /**
     * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
     */
    private StringBuilder mComposingText = new StringBuilder();
    /**
     * This is a one-character string containing the character after the cursor. Since LatinIME
     * never touches it directly, it's never modified by any means other than re-reading from the
     * TextView when the cursor position is changed by the user.
     */
    private CharSequence mCharAfterTheCursor = "";
    // A hint on how many characters to cache from the TextView. A good value of this is given by
    // how many characters we need to be able to almost always find the caps mode.
    private static final int DEFAULT_TEXT_CACHE_SIZE = 100;

    private final InputMethodService mParent;
    InputConnection mIC;
    int mNestLevel;
@@ -52,6 +83,37 @@ public class RichInputConnection {
        mNestLevel = 0;
    }

    private void checkConsistencyForDebug() {
        final ExtractedTextRequest r = new ExtractedTextRequest();
        r.hintMaxChars = 0;
        r.hintMaxLines = 0;
        r.token = 1;
        r.flags = 0;
        final ExtractedText et = mIC.getExtractedText(r, 0);
        final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
        final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
                .append(mComposingText);
        if (null == et || null == beforeCursor) return;
        final int actualLength = Math.min(beforeCursor.length(), internal.length());
        if (internal.length() > actualLength) {
            internal.delete(0, internal.length() - actualLength);
        }
        final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
                : beforeCursor.subSequence(beforeCursor.length() - actualLength,
                        beforeCursor.length()).toString();
        if (et.selectionStart != mCurrentCursorPosition
                || !(reference.equals(internal.toString()))) {
            final String context = "Expected cursor position = " + mCurrentCursorPosition
                    + "\nActual cursor position = " + et.selectionStart
                    + "\nExpected text = " + internal.length() + " " + internal
                    + "\nActual text = " + reference.length() + " " + reference;
            ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
        } else {
            Log.e(TAG, Utils.getStackTrace(2));
            Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
        }
    }

    public void beginBatchEdit() {
        if (++mNestLevel == 1) {
            mIC = mParent.getCurrentInputConnection();
@@ -65,12 +127,30 @@ public class RichInputConnection {
                Log.e(TAG, "Nest level too deep : " + mNestLevel);
            }
        }
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public void endBatchEdit() {
        if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
        if (--mNestLevel == 0 && null != mIC) {
            mIC.endBatchEdit();
        }
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public void resetCachesUponCursorMove(final int newCursorPosition) {
        mCurrentCursorPosition = newCursorPosition;
        mComposingText.setLength(0);
        mCommittedTextBeforeComposingText.setLength(0);
        mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
        mCharAfterTheCursor = getTextAfterCursor(1, 0);
        if (null != mIC) {
            mIC.finishComposingText();
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_finishComposingText();
            }
        }
    }

    private void checkBatchEdit() {
@@ -83,6 +163,10 @@ public class RichInputConnection {

    public void finishComposingText() {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        mCommittedTextBeforeComposingText.append(mComposingText);
        mCurrentCursorPosition += mComposingText.length();
        mComposingText.setLength(0);
        if (null != mIC) {
            mIC.finishComposingText();
            if (ProductionFlag.IS_EXPERIMENTAL) {
@@ -93,6 +177,10 @@ public class RichInputConnection {

    public void commitText(final CharSequence text, final int i) {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        mCommittedTextBeforeComposingText.append(text);
        mCurrentCursorPosition += text.length() - mComposingText.length();
        mComposingText.setLength(0);
        if (null != mIC) {
            mIC.commitText(text, i);
            if (ProductionFlag.IS_EXPERIMENTAL) {
@@ -121,12 +209,28 @@ public class RichInputConnection {

    public void deleteSurroundingText(final int i, final int j) {
        checkBatchEdit();
        final int remainingChars = mComposingText.length() - i;
        if (remainingChars >= 0) {
            mComposingText.setLength(remainingChars);
        } else {
            mComposingText.setLength(0);
            // Never cut under 0
            final int len = Math.max(mCommittedTextBeforeComposingText.length()
                    + remainingChars, 0);
            mCommittedTextBeforeComposingText.setLength(len);
        }
        if (mCurrentCursorPosition > i) {
            mCurrentCursorPosition -= i;
        } else {
            mCurrentCursorPosition = 0;
        }
        if (null != mIC) {
            mIC.deleteSurroundingText(i, j);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
            }
        }
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public void performEditorAction(final int actionId) {
@@ -141,6 +245,44 @@ public class RichInputConnection {

    public void sendKeyEvent(final KeyEvent keyEvent) {
        checkBatchEdit();
        if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
            if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
            // This method is only called for enter or backspace when speaking to old
            // applications (target SDK <= 15), or for digits.
            // When talking to new applications we never use this method because it's inherently
            // racy and has unpredictable results, but for backward compatibility we continue
            // sending the key events for only Enter and Backspace because some applications
            // mistakenly catch them to do some stuff.
            switch (keyEvent.getKeyCode()) {
                case KeyEvent.KEYCODE_ENTER:
                    mCommittedTextBeforeComposingText.append("\n");
                    mCurrentCursorPosition += 1;
                    break;
                case KeyEvent.KEYCODE_DEL:
                    if (0 == mComposingText.length()) {
                        if (mCommittedTextBeforeComposingText.length() > 0) {
                            mCommittedTextBeforeComposingText.delete(
                                    mCommittedTextBeforeComposingText.length() - 1,
                                    mCommittedTextBeforeComposingText.length());
                        }
                    } else {
                        mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
                    }
                    if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
                    break;
                case KeyEvent.KEYCODE_UNKNOWN:
                    if (null != keyEvent.getCharacters()) {
                        mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
                        mCurrentCursorPosition += keyEvent.getCharacters().length();
                    }
                    break;
                default:
                    final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
                    mCommittedTextBeforeComposingText.append(text);
                    mCurrentCursorPosition += text.length();
                    break;
            }
        }
        if (null != mIC) {
            mIC.sendKeyEvent(keyEvent);
            if (ProductionFlag.IS_EXPERIMENTAL) {
@@ -151,48 +293,83 @@ public class RichInputConnection {

    public void setComposingText(final CharSequence text, final int i) {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        mCurrentCursorPosition += text.length() - mComposingText.length();
        mComposingText.setLength(0);
        mComposingText.append(text);
        // TODO: support values of i != 1. At this time, this is never called with i != 1.
        if (null != mIC) {
            mIC.setComposingText(text, i);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_setComposingText(text, i);
            }
        }
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public void setSelection(final int from, final int to) {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        if (null != mIC) {
            mIC.setSelection(from, to);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_setSelection(from, to);
            }
        }
        mCurrentCursorPosition = from;
        mCommittedTextBeforeComposingText.setLength(0);
        mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
    }

    public void commitCorrection(final CorrectionInfo correctionInfo) {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        // This has no effect on the text field and does not change its content. It only makes
        // TextView flash the text for a second based on indices contained in the argument.
        if (null != mIC) {
            mIC.commitCorrection(correctionInfo);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
            }
        }
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public void commitCompletion(final CompletionInfo completionInfo) {
        checkBatchEdit();
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
        final CharSequence text = completionInfo.getText();
        mCommittedTextBeforeComposingText.append(text);
        mCurrentCursorPosition += text.length() - mComposingText.length();
        mComposingText.setLength(0);
        if (null != mIC) {
            mIC.commitCompletion(completionInfo);
            if (ProductionFlag.IS_EXPERIMENTAL) {
                ResearchLogger.richInputConnection_commitCompletion(completionInfo);
            }
        }
        if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    }

    public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
        mIC = mParent.getCurrentInputConnection();
        if (null == mIC) return null;
        final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
        if (DEBUG_PREVIOUS_TEXT && null != prev) {
            final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
            final String reference = prev.length() <= checkLength ? prev.toString()
                    : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
            final StringBuilder internal = new StringBuilder()
                    .append(mCommittedTextBeforeComposingText).append(mComposingText);
            if (internal.length() > checkLength) {
                internal.delete(0, internal.length() - checkLength);
                if (!(reference.equals(internal.toString()))) {
                    final String context =
                            "Expected text = " + internal + "\nActual text = " + reference;
                    ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
                }
            }
        }
        return getNthPreviousWord(prev, sentenceSeperators, n);
    }