aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/RichInputConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin/RichInputConnection.java')
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java249
1 files changed, 116 insertions, 133 deletions
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 744b0321a..08e8fe346 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -16,13 +16,14 @@
package com.android.inputmethod.latin;
-import android.graphics.Color;
+import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH;
+
import android.inputmethodservice.InputMethodService;
import android.os.Build;
+import android.os.Bundle;
import android.text.SpannableStringBuilder;
-import android.text.Spanned;
import android.text.TextUtils;
-import android.text.style.BackgroundColorSpan;
+import android.text.style.CharacterStyle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.inputmethod.CompletionInfo;
@@ -33,16 +34,20 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import com.android.inputmethod.compat.InputConnectionCompatUtils;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.UnicodeSurrogate;
+import com.android.inputmethod.latin.common.StringUtils;
+import com.android.inputmethod.latin.define.DecoderSpecificConstants;
+import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.CapsModeUtils;
import com.android.inputmethod.latin.utils.DebugLogUtils;
-import com.android.inputmethod.latin.utils.PrevWordsInfoUtils;
+import com.android.inputmethod.latin.utils.NgramContextUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.SpannableStringUtils;
-import com.android.inputmethod.latin.utils.StringUtils;
import com.android.inputmethod.latin.utils.TextRange;
-import java.util.Arrays;
+import javax.annotation.Nonnull;
/**
* Enrichment class for InputConnection to simplify interaction and add functionality.
@@ -52,15 +57,15 @@ import java.util.Arrays;
* all the time to find out what text is in the buffer, when we need it to determine caps mode
* for example.
*/
-public final class RichInputConnection {
+public final class RichInputConnection implements PrivateCommandPerformer {
private static final String TAG = RichInputConnection.class.getSimpleName();
private static final boolean DBG = false;
private static final boolean DEBUG_PREVIOUS_TEXT = false;
private static final boolean DEBUG_BATCH_NESTING = false;
// Provision for long words and separators between the words.
- private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH
- * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */
- + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */;
+ private static final int LOOKBACK_CHARACTER_NUM = DICTIONARY_MAX_WORD_LENGTH
+ * (DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */
+ + DecoderSpecificConstants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */;
private static final int INVALID_CURSOR_POSITION = -1;
/**
@@ -88,26 +93,25 @@ public final class RichInputConnection {
private final StringBuilder mComposingText = new StringBuilder();
/**
- * This variable is a temporary object used in
- * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation.
+ * This variable is a temporary object used in {@link #commitText(CharSequence,int)}
+ * to avoid object creation.
*/
private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
- /**
- * This variable is used to track whether the last committed text had the background color or
- * not.
- * TODO: Omit this flag if possible.
- */
- private boolean mLastCommittedTextHasBackgroundColor = false;
private final InputMethodService mParent;
InputConnection mIC;
int mNestLevel;
+
public RichInputConnection(final InputMethodService parent) {
mParent = parent;
mIC = null;
mNestLevel = 0;
}
+ public boolean isConnected() {
+ return mIC != null;
+ }
+
private void checkConsistencyForDebug() {
final ExtractedTextRequest r = new ExtractedTextRequest();
r.hintMaxChars = 0;
@@ -143,15 +147,14 @@ public final class RichInputConnection {
public void beginBatchEdit() {
if (++mNestLevel == 1) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) {
+ if (isConnected()) {
mIC.beginBatchEdit();
}
} else {
if (DBG) {
throw new RuntimeException("Nest level too deep");
- } else {
- Log.e(TAG, "Nest level too deep : " + mNestLevel);
}
+ Log.e(TAG, "Nest level too deep : " + mNestLevel);
}
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -159,7 +162,7 @@ public final class RichInputConnection {
public void endBatchEdit() {
if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
- if (--mNestLevel == 0 && null != mIC) {
+ if (--mNestLevel == 0 && isConnected()) {
mIC.endBatchEdit();
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -191,7 +194,7 @@ public final class RichInputConnection {
Log.d(TAG, "Will try to retrieve text later.");
return false;
}
- if (null != mIC && shouldFinishComposition) {
+ if (isConnected() && shouldFinishComposition) {
mIC.finishComposingText();
}
return true;
@@ -207,8 +210,9 @@ public final class RichInputConnection {
mIC = mParent.getCurrentInputConnection();
// Call upon the inputconnection directly since our own method is using the cache, and
// we want to refresh it.
- final CharSequence textBeforeCursor = null == mIC ? null :
- mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+ final CharSequence textBeforeCursor = isConnected()
+ ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0)
+ : null;
if (null == textBeforeCursor) {
// For some reason the app thinks we are not connected to it. This looks like a
// framework bug... Fall back to ground state and return false.
@@ -237,39 +241,18 @@ public final class RichInputConnection {
// it works, but it's wrong and should be fixed.
mCommittedTextBeforeComposingText.append(mComposingText);
mComposingText.setLength(0);
- // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed.
- mLastCommittedTextHasBackgroundColor = false;
- if (null != mIC) {
+ if (isConnected()) {
mIC.finishComposingText();
}
}
/**
- * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}.
+ * Calls {@link InputConnection#commitText(CharSequence, int)}.
+ *
* @param text The text to commit. This may include styles.
- * See {@link InputConnection#commitText(CharSequence, int)}.
* @param newCursorPosition The new cursor position around the text.
- * See {@link InputConnection#commitText(CharSequence, int)}.
*/
public void commitText(final CharSequence text, final int newCursorPosition) {
- commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT, text.length());
- }
-
- /**
- * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color.
- * @param text The text to commit. This may include styles.
- * See {@link InputConnection#commitText(CharSequence, int)}.
- * @param newCursorPosition The new cursor position around the text.
- * See {@link InputConnection#commitText(CharSequence, int)}.
- * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable
- * the background color. Note that this method specifies {@link BackgroundColorSpan} with
- * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until
- * {@link #finishComposingText()} is called.
- * @param coloredTextLength the length of text, in Java chars, which should be rendered with
- * the given background color.
- */
- public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition,
- final int color, final int coloredTextLength) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(text);
@@ -279,46 +262,34 @@ public final class RichInputConnection {
mExpectedSelStart += text.length() - mComposingText.length();
mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
- mLastCommittedTextHasBackgroundColor = false;
- if (null != mIC) {
- if (color == Color.TRANSPARENT) {
- mIC.commitText(text, newCursorPosition);
- } else {
- mTempObjectForCommitText.clear();
- mTempObjectForCommitText.append(text);
- final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color);
- final int spanLength = Math.min(coloredTextLength, text.length());
- mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, spanLength,
- Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- mIC.commitText(mTempObjectForCommitText, newCursorPosition);
- mLastCommittedTextHasBackgroundColor = true;
+ if (isConnected()) {
+ mTempObjectForCommitText.clear();
+ mTempObjectForCommitText.append(text);
+ final CharacterStyle[] spans = mTempObjectForCommitText.getSpans(
+ 0, text.length(), CharacterStyle.class);
+ for (final CharacterStyle span : spans) {
+ final int spanStart = mTempObjectForCommitText.getSpanStart(span);
+ final int spanEnd = mTempObjectForCommitText.getSpanEnd(span);
+ final int spanFlags = mTempObjectForCommitText.getSpanFlags(span);
+ // We have to adjust the end of the span to include an additional character.
+ // This is to avoid splitting a unicode surrogate pair.
+ // See com.android.inputmethod.latin.common.Constants.UnicodeSurrogate
+ // See https://b.corp.google.com/issues/19255233
+ if (0 < spanEnd && spanEnd < mTempObjectForCommitText.length()) {
+ final char spanEndChar = mTempObjectForCommitText.charAt(spanEnd - 1);
+ final char nextChar = mTempObjectForCommitText.charAt(spanEnd);
+ if (UnicodeSurrogate.isLowSurrogate(spanEndChar)
+ && UnicodeSurrogate.isHighSurrogate(nextChar)) {
+ mTempObjectForCommitText.setSpan(span, spanStart, spanEnd + 1, spanFlags);
+ }
+ }
}
+ mIC.commitText(mTempObjectForCommitText, newCursorPosition);
}
}
- /**
- * Removes the background color from the highlighted text if necessary. Should be called while
- * there is no on-going composing text.
- *
- * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}.
- * Be careful of any unexpected side effects.</p>
- */
- public void removeBackgroundColorFromHighlightedTextIfNecessary() {
- // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this
- // flag if everything works fine without this condition.
- if (!mLastCommittedTextHasBackgroundColor) {
- return;
- }
- if (mComposingText.length() > 0) {
- Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " +
- "empty. mComposingText=" + mComposingText);
- return;
- }
- finishComposingText();
- }
-
public CharSequence getSelectedText(final int flags) {
- return (null == mIC) ? null : mIC.getSelectedText(flags);
+ return isConnected() ? mIC.getSelectedText(flags) : null;
}
public boolean canDeleteCharacters() {
@@ -343,16 +314,17 @@ public final class RichInputConnection {
public int getCursorCapsMode(final int inputType,
final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
mIC = mParent.getCurrentInputConnection();
- if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+ if (!isConnected()) {
+ return Constants.TextUtils.CAP_MODE_OFF;
+ }
if (!TextUtils.isEmpty(mComposingText)) {
if (hasSpaceBefore) {
// If we have some composing text and a space before, then we should have
// MODE_CHARACTERS and MODE_WORDS on.
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
- } else {
- // We have some composing text - we should be in MODE_CHARACTERS only.
- return TextUtils.CAP_MODE_CHARACTERS & inputType;
}
+ // We have some composing text - we should be in MODE_CHARACTERS only.
+ return TextUtils.CAP_MODE_CHARACTERS & inputType;
}
// TODO: this will generally work, but there may be cases where the buffer contains SOME
// information but not enough to determine the caps mode accurately. This may happen after
@@ -367,7 +339,9 @@ public final class RichInputConnection {
}
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
// never blocks or initiates IPC.
- return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
+ // TODO: don't call #toString() here. Instead, all accesses to
+ // mCommittedTextBeforeComposingText should be done on the main thread.
+ return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText.toString(), inputType,
spacingAndPunctuations, hasSpaceBefore);
}
@@ -402,12 +376,12 @@ public final class RichInputConnection {
return s;
}
mIC = mParent.getCurrentInputConnection();
- return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags);
+ return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null;
}
public CharSequence getTextAfterCursor(final int n, final int flags) {
mIC = mParent.getCurrentInputConnection();
- return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags);
+ return isConnected() ? mIC.getTextAfterCursor(n, flags) : null;
}
public void deleteSurroundingText(final int beforeLength, final int afterLength) {
@@ -434,7 +408,7 @@ public final class RichInputConnection {
mExpectedSelEnd -= mExpectedSelStart;
mExpectedSelStart = 0;
}
- if (null != mIC) {
+ if (isConnected()) {
mIC.deleteSurroundingText(beforeLength, afterLength);
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -442,7 +416,7 @@ public final class RichInputConnection {
public void performEditorAction(final int actionId) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) {
+ if (isConnected()) {
mIC.performEditorAction(actionId);
}
}
@@ -494,7 +468,7 @@ public final class RichInputConnection {
break;
}
}
- if (null != mIC) {
+ if (isConnected()) {
mIC.sendKeyEvent(keyEvent);
}
}
@@ -517,7 +491,7 @@ public final class RichInputConnection {
mCommittedTextBeforeComposingText.append(
textBeforeCursor.subSequence(0, indexOfStartOfComposingText));
}
- if (null != mIC) {
+ if (isConnected()) {
mIC.setComposingRegion(start, end);
}
}
@@ -531,7 +505,7 @@ public final class RichInputConnection {
mComposingText.append(text);
// TODO: support values of newCursorPosition != 1. At this time, this is never called with
// newCursorPosition != 1.
- if (null != mIC) {
+ if (isConnected()) {
mIC.setComposingText(text, newCursorPosition);
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -556,7 +530,7 @@ public final class RichInputConnection {
}
mExpectedSelStart = start;
mExpectedSelEnd = end;
- if (null != mIC) {
+ if (isConnected()) {
final boolean isIcValid = mIC.setSelection(start, end);
if (!isIcValid) {
return false;
@@ -570,7 +544,7 @@ public final class RichInputConnection {
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) {
+ if (isConnected()) {
mIC.commitCorrection(correctionInfo);
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -586,18 +560,19 @@ public final class RichInputConnection {
mExpectedSelStart += text.length() - mComposingText.length();
mExpectedSelEnd = mExpectedSelStart;
mComposingText.setLength(0);
- if (null != mIC) {
+ if (isConnected()) {
mIC.commitCompletion(completionInfo);
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
@SuppressWarnings("unused")
- public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(
+ @Nonnull
+ public NgramContext getNgramContextFromNthPreviousWord(
final SpacingAndPunctuations spacingAndPunctuations, final int n) {
mIC = mParent.getCurrentInputConnection();
- if (null == mIC) {
- return PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
+ if (!isConnected()) {
+ return NgramContext.EMPTY_PREV_WORDS_INFO;
}
final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
if (DEBUG_PREVIOUS_TEXT && null != prev) {
@@ -618,14 +593,10 @@ public final class RichInputConnection {
}
}
}
- return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord(
+ return NgramContextUtils.getNgramContextFromNthPreviousWord(
prev, spacingAndPunctuations, n);
}
- private static boolean isSeparator(final int code, final int[] sortedSeparators) {
- return Arrays.binarySearch(sortedSeparators, code) >= 0;
- }
-
private static boolean isPartOfCompositionForScript(final int codePoint,
final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) {
// We always consider word connectors part of compositions.
@@ -645,7 +616,7 @@ public final class RichInputConnection {
public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
final int scriptId) {
mIC = mParent.getCurrentInputConnection();
- if (mIC == null) {
+ if (!isConnected()) {
return null;
}
final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
@@ -740,17 +711,19 @@ public final class RichInputConnection {
return TextUtils.equals(text, beforeText);
}
- public boolean revertDoubleSpacePeriod() {
+ public boolean revertDoubleSpacePeriod(final SpacingAndPunctuations spacingAndPunctuations) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
// Here we test whether we indeed have a period and a space before us. This should not
// be needed, but it's there just in case something went wrong.
final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
- if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) {
+ if (!TextUtils.equals(spacingAndPunctuations.mSentenceSeparatorAndSpace,
+ textBeforeCursor)) {
// Theoretically we should not be coming here if there isn't ". " before the
// cursor, but the application may be changing the text while we are typing, so
// anything goes. We should not crash.
- Log.d(TAG, "Tried to revert double-space combo but we didn't find "
- + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor.");
+ Log.d(TAG, "Tried to revert double-space combo but we didn't find \""
+ + spacingAndPunctuations.mSentenceSeparatorAndSpace
+ + "\" just before the cursor.");
return false;
}
// Double-space results in ". ". A backspace to cancel this should result in a single
@@ -847,17 +820,32 @@ public final class RichInputConnection {
/**
* Try to get the text from the editor to expose lies the framework may have been
- * telling us. Concretely, when the device rotates, the frameworks tells us about where the
- * cursor used to be initially in the editor at the time it first received the focus; this
+ * telling us. Concretely, when the device rotates and when the keyboard reopens in the same
+ * text field after having been closed with the back key, the frameworks tells us about where
+ * the cursor used to be initially in the editor at the time it first received the focus; this
* may be completely different from the place it is upon rotation. Since we don't have any
* means to get the real value, try at least to ask the text view for some characters and
* detect the most damaging cases: when the cursor position is declared to be much smaller
* than it really is.
*/
public void tryFixLyingCursorPosition() {
+ mIC = mParent.getCurrentInputConnection();
final CharSequence textBeforeCursor = getTextBeforeCursor(
Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
- if (null == textBeforeCursor) {
+ final CharSequence selectedText = isConnected() ? mIC.getSelectedText(0 /* flags */) : null;
+ if (null == textBeforeCursor ||
+ (!TextUtils.isEmpty(selectedText) && mExpectedSelEnd == mExpectedSelStart)) {
+ // If textBeforeCursor is null, we have no idea what kind of text field we have or if
+ // thinking about the "cursor position" actually makes any sense. In this case we
+ // remember a meaningless cursor position. Contrast this with an empty string, which is
+ // valid and should mean the cursor is at the start of the text.
+ // Also, if we expect we don't have a selection but we DO have non-empty selected text,
+ // then the framework lied to us about the cursor position. In this case, we should just
+ // revert to the most basic behavior possible for the next action (backspace in
+ // particular comes to mind), so we remember a meaningless cursor position which should
+ // result in degraded behavior from the next input.
+ // Interestingly, in either case, chances are any action the user takes next will result
+ // in a call to onUpdateSelection, which should set things right.
mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
} else {
final int textLength = textBeforeCursor.length();
@@ -880,6 +868,15 @@ public final class RichInputConnection {
}
}
+ @Override
+ public boolean performPrivateCommand(final String action, final Bundle data) {
+ mIC = mParent.getCurrentInputConnection();
+ if (!isConnected()) {
+ return false;
+ }
+ return mIC.performPrivateCommand(action, data);
+ }
+
public int getExpectedSelectionStart() {
return mExpectedSelStart;
}
@@ -920,8 +917,6 @@ public final class RichInputConnection {
}
}
- private boolean mCursorAnchorInfoMonitorEnabled = false;
-
/**
* Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}.
* @param enableMonitor {@code true} to request the editor to call back the method whenever the
@@ -936,22 +931,10 @@ public final class RichInputConnection {
public boolean requestCursorUpdates(final boolean enableMonitor,
final boolean requestImmediateCallback) {
mIC = mParent.getCurrentInputConnection();
- final boolean scheduled;
- if (null != mIC) {
- scheduled = InputConnectionCompatUtils.requestCursorUpdates(mIC, enableMonitor,
- requestImmediateCallback);
- } else {
- scheduled = false;
+ if (!isConnected()) {
+ return false;
}
- mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor);
- return scheduled;
- }
-
- /**
- * @return {@code true} if the application reported that the monitor mode of
- * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled.
- */
- public boolean isCursorAnchorInfoMonitorEnabled() {
- return mCursorAnchorInfoMonitorEnabled;
+ return InputConnectionCompatUtils.requestCursorUpdates(
+ mIC, enableMonitor, requestImmediateCallback);
}
}