diff options
author | 2024-12-16 21:45:41 -0500 | |
---|---|---|
committer | 2025-01-11 14:17:35 -0500 | |
commit | e9a0e66716dab4dd3184d009d8920de1961efdfa (patch) | |
tree | 02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/com/android/inputmethod/latin/RichInputConnection.java | |
parent | fb3b9360d70596d7e921de8bf7d3ca99564a077e (diff) | |
download | latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip |
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/com/android/inputmethod/latin/RichInputConnection.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/RichInputConnection.java | 1033 |
1 files changed, 0 insertions, 1033 deletions
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java deleted file mode 100644 index a10f2bdb0..000000000 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ /dev/null @@ -1,1033 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.inputmethodservice.InputMethodService; -import android.os.Build; -import android.os.Bundle; -import android.os.SystemClock; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.CharacterStyle; -import android.util.Log; -import android.view.KeyEvent; -import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.CorrectionInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; -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.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.NgramContextUtils; -import com.android.inputmethod.latin.utils.ScriptUtils; -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; - -/** - * 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 final class RichInputConnection implements PrivateCommandPerformer { - private static final String TAG = "RichInputConnection"; - private static final boolean DBG = false; - private static final boolean DEBUG_PREVIOUS_TEXT = false; - private static final boolean DEBUG_BATCH_NESTING = false; - 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; - - /** - * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter - * the {@link #hasSlowInputConnection} state. - */ - private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000; - /** - * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs - * to take for the keyboard to enter the {@link #hasSlowInputConnection} state. - */ - private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_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; - private static final int OPERATION_RELOAD_TEXT_CACHE = 3; - private static final String[] OPERATION_NAMES = new String[] { - "GET_TEXT_BEFORE_CURSOR", - "GET_TEXT_AFTER_CURSOR", - "GET_WORD_RANGE_AT_CURSOR", - "RELOAD_TEXT_CACHE"}; - - /** - * The amount of time the keyboard will persist in the {@link #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 - * a keyboard command or not. - * It's not really the selection start position: the selection start may not be there yet, and - * in some cases, it may never arrive there. - */ - private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points - /** - * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is - * expected. The same caveats as mExpectedSelStart apply. - */ - private int mExpectedSelEnd = 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 final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder(); - /** - * This contains the currently composing text, as LatinIME thinks the TextView is seeing it. - */ - private final StringBuilder mComposingText = new StringBuilder(); - - /** - * This variable is a temporary object used in {@link #commitText(CharSequence,int)} - * to avoid object creation. - */ - private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder(); - - private final InputMethodService mParent; - private InputConnection mIC; - private int mNestLevel; - - /** - * The timestamp of the last slow InputConnection operation - */ - private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; - - public RichInputConnection(final InputMethodService parent) { - mParent = parent; - mIC = null; - mNestLevel = 0; - } - - public boolean isConnected() { - 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 (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime) - <= SLOW_INPUTCONNECTION_PERSIST_MS; - } - - public void onStartInput() { - mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS; - } - - 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(Constants.EDITOR_CONTENTS_CACHE_SIZE, - 0); - final StringBuilder internal = new StringBuilder(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 != mExpectedSelStart - || !(reference.equals(internal.toString()))) { - final String context = "Expected selection start = " + mExpectedSelStart - + "\nActual selection start = " + et.selectionStart - + "\nExpected text = " + internal.length() + " " + internal - + "\nActual text = " + reference.length() + " " + reference; - ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); - } else { - Log.e(TAG, DebugLogUtils.getStackTrace(2)); - Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart); - } - } - - public void beginBatchEdit() { - if (++mNestLevel == 1) { - mIC = mParent.getCurrentInputConnection(); - if (isConnected()) { - mIC.beginBatchEdit(); - } - } else { - if (DBG) { - throw new RuntimeException("Nest level too deep"); - } - Log.e(TAG, "Nest level too deep : " + mNestLevel); - } - if (DEBUG_BATCH_NESTING) 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 && isConnected()) { - mIC.endBatchEdit(); - } - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - } - - /** - * Reset the cached text and retrieve it again from the editor. - * - * This should be called when the cursor moved. It's possible that we can't connect to - * the application when doing this; notably, this happens sometimes during rotation, probably - * because of a race condition in the framework. In this case, we just can't retrieve the - * data, so we empty the cache and note that we don't know the new cursor position, and we - * return false so that the caller knows about this and can retry later. - * - * @param newSelStart the new position of the selection start, as received from the system. - * @param newSelEnd the new position of the selection end, as received from the system. - * @param shouldFinishComposition whether we should finish the composition in progress. - * @return true if we were able to connect to the editor successfully, false otherwise. When - * this method returns false, the caches could not be correctly refreshed so they were only - * reset: the caller should try again later to return to normal operation. - */ - public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart, - final int newSelEnd, final boolean shouldFinishComposition) { - mExpectedSelStart = newSelStart; - mExpectedSelEnd = newSelEnd; - mComposingText.setLength(0); - final boolean didReloadTextSuccessfully = reloadTextCache(); - if (!didReloadTextSuccessfully) { - Log.d(TAG, "Will try to retrieve text later."); - return false; - } - if (isConnected() && shouldFinishComposition) { - mIC.finishComposingText(); - } - return true; - } - - /** - * Reload the cached text from the InputConnection. - * - * @return true if successful - */ - private boolean reloadTextCache() { - mCommittedTextBeforeComposingText.setLength(0); - 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 = getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_RELOAD_TEXT_CACHE, - SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS, - Constants.EDITOR_CONTENTS_CACHE_SIZE, - 0 /* flags */); - 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. - mExpectedSelStart = INVALID_CURSOR_POSITION; - mExpectedSelEnd = INVALID_CURSOR_POSITION; - Log.e(TAG, "Unable to connect to the editor to retrieve text."); - return false; - } - mCommittedTextBeforeComposingText.append(textBeforeCursor); - return true; - } - - private void checkBatchEdit() { - if (mNestLevel != 1) { - // TODO: exception instead - Log.e(TAG, "Batch edit level incorrect : " + mNestLevel); - Log.e(TAG, DebugLogUtils.getStackTrace(4)); - } - } - - public void finishComposingText() { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - // TODO: this is not correct! The cursor is not necessarily after the composing text. - // In the practice right now this is only called when input ends so it will be reset so - // it works, but it's wrong and should be fixed. - mCommittedTextBeforeComposingText.append(mComposingText); - mComposingText.setLength(0); - if (isConnected()) { - mIC.finishComposingText(); - } - } - - /** - * Calls {@link InputConnection#commitText(CharSequence, int)}. - * - * @param text The text to commit. This may include styles. - * @param newCursorPosition The new cursor position around the text. - */ - public void commitText(final CharSequence text, final int newCursorPosition) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - mCommittedTextBeforeComposingText.append(text); - // TODO: the following is exceedingly error-prone. Right now when the cursor is in the - // middle of the composing word mComposingText only holds the part of the composing text - // that is before the cursor, so this actually works, but it's terribly confusing. Fix this. - mExpectedSelStart += text.length() - mComposingText.length(); - mExpectedSelEnd = mExpectedSelStart; - mComposingText.setLength(0); - 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); - } - } - - @Nullable - public CharSequence getSelectedText(final int flags) { - return isConnected() ? mIC.getSelectedText(flags) : null; - } - - public boolean canDeleteCharacters() { - return mExpectedSelStart > 0; - } - - /** - * Gets the caps modes we should be in after this specific string. - * - * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument. - * This method also supports faking an additional space after the string passed in argument, - * to support cases where a space will be added automatically, like in phantom space - * state for example. - * Note that for English, we are using American typography rules (which are not specific to - * American English, it's just the most common set of rules for English). - * - * @param inputType a mask of the caps modes to test for. - * @param spacingAndPunctuations the values of the settings to use for locale and separators. - * @param hasSpaceBefore if we should consider there should be a space after the string. - * @return the caps modes that should be on as a set of bits - */ - public int getCursorCapsMode(final int inputType, - final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { - mIC = mParent.getCurrentInputConnection(); - 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; - } - // 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 - // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. - // getCapsMode should be updated to be able to return a "not enough info" result so that - // we can get more context only when needed. - if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) { - if (!reloadTextCache()) { - Log.w(TAG, "Unable to connect to the editor. " - + "Setting caps mode without knowing text."); - } - } - // This never calls InputConnection#getCapsMode - in fact, it's a static method that - // never blocks or initiates IPC. - // 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); - } - - public int getCodePointBeforeCursor() { - final int length = mCommittedTextBeforeComposingText.length(); - if (length < 1) return Constants.NOT_A_CODE; - return Character.codePointBefore(mCommittedTextBeforeComposingText, length); - } - - public CharSequence getTextBeforeCursor(final int n, final int flags) { - final int cachedLength = - mCommittedTextBeforeComposingText.length() + mComposingText.length(); - // If we have enough characters to satisfy the request, or if we have all characters in - // the text field, then we can return the cached version right away. - // However, if we don't have an expected cursor position, then we should always - // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to - // test for this explicitly) - if (INVALID_CURSOR_POSITION != mExpectedSelStart - && (cachedLength >= n || cachedLength >= mExpectedSelStart)) { - final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText); - // We call #toString() here to create a temporary object. - // In some situations, this method is called on a worker thread, and it's possible - // the main thread touches the contents of mComposingText while this worker thread - // is suspended, because mComposingText is a StringBuilder. This may lead to crashes, - // so we call #toString() on it. That will result in the return value being strictly - // speaking wrong, but since this is used for basing bigram probability off, and - // it's only going to matter for one getSuggestions call, it's fine in the practice. - s.append(mComposingText.toString()); - if (s.length() > n) { - s.delete(0, s.length() - n); - } - return s; - } - return getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_GET_TEXT_BEFORE_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - n, flags); - } - - private CharSequence getTextBeforeCursorAndDetectLaggyConnection( - final int operation, final long timeout, final int n, final int flags) { - mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return null; - } - final long startTime = SystemClock.uptimeMillis(); - final CharSequence result = mIC.getTextBeforeCursor(n, flags); - detectLaggyConnection(operation, timeout, startTime); - return result; - } - - public CharSequence getTextAfterCursor(final int n, final int flags) { - return getTextAfterCursorAndDetectLaggyConnection( - OPERATION_GET_TEXT_AFTER_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - n, flags); - } - - private CharSequence getTextAfterCursorAndDetectLaggyConnection( - final int operation, final long timeout, final int n, final int flags) { - mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return null; - } - final long startTime = SystemClock.uptimeMillis(); - final CharSequence result = mIC.getTextAfterCursor(n, flags); - detectLaggyConnection(operation, timeout, startTime); - return result; - } - - private void detectLaggyConnection(final int operation, final long timeout, final long startTime) { - final long duration = SystemClock.uptimeMillis() - startTime; - if (duration >= timeout) { - final String operationName = OPERATION_NAMES[operation]; - Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms."); - StatsUtils.onInputConnectionLaggy(operation, duration); - mLastSlowInputConnectionTime = SystemClock.uptimeMillis(); - } - } - - public void deleteTextBeforeCursor(final int beforeLength) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - // TODO: the following is incorrect if the cursor is not immediately after the composition. - // Right now we never come here in this case because we reset the composing state before we - // come here in this case, but we need to fix this. - final int remainingChars = mComposingText.length() - beforeLength; - 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 (mExpectedSelStart > beforeLength) { - mExpectedSelStart -= beforeLength; - mExpectedSelEnd -= beforeLength; - } else { - // There are fewer characters before the cursor in the buffer than we are being asked to - // delete. Only delete what is there, and update the end with the amount deleted. - mExpectedSelEnd -= mExpectedSelStart; - mExpectedSelStart = 0; - } - if (isConnected()) { - mIC.deleteSurroundingText(beforeLength, 0); - } - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - } - - public void performEditorAction(final int actionId) { - mIC = mParent.getCurrentInputConnection(); - if (isConnected()) { - mIC.performEditorAction(actionId); - } - } - - public void sendKeyEvent(final KeyEvent keyEvent) { - if (DEBUG_BATCH_NESTING) 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 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), 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"); - mExpectedSelStart += 1; - mExpectedSelEnd = mExpectedSelStart; - 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 (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) { - // TODO: Handle surrogate pairs. - mExpectedSelStart -= 1; - } - mExpectedSelEnd = mExpectedSelStart; - break; - case KeyEvent.KEYCODE_UNKNOWN: - if (null != keyEvent.getCharacters()) { - mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); - mExpectedSelStart += keyEvent.getCharacters().length(); - mExpectedSelEnd = mExpectedSelStart; - } - break; - default: - final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar()); - mCommittedTextBeforeComposingText.append(text); - mExpectedSelStart += text.length(); - mExpectedSelEnd = mExpectedSelStart; - break; - } - } - if (isConnected()) { - mIC.sendKeyEvent(keyEvent); - } - } - - public void setComposingRegion(final int start, final int end) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - final CharSequence textBeforeCursor = - getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0); - mCommittedTextBeforeComposingText.setLength(0); - if (!TextUtils.isEmpty(textBeforeCursor)) { - // The cursor is not necessarily at the end of the composing text, but we have its - // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start - // of the text, so we should use mExpectedSelStart. In other words, the composing - // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor - final int indexOfStartOfComposingText = - Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0); - mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, - textBeforeCursor.length())); - mCommittedTextBeforeComposingText.append( - textBeforeCursor.subSequence(0, indexOfStartOfComposingText)); - } - if (isConnected()) { - mIC.setComposingRegion(start, end); - } - } - - public void setComposingText(final CharSequence text, final int newCursorPosition) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - mExpectedSelStart += text.length() - mComposingText.length(); - mExpectedSelEnd = mExpectedSelStart; - mComposingText.setLength(0); - mComposingText.append(text); - // TODO: support values of newCursorPosition != 1. At this time, this is never called with - // newCursorPosition != 1. - if (isConnected()) { - mIC.setComposingText(text, newCursorPosition); - } - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - } - - /** - * Set the selection of the text editor. - * - * Calls through to {@link InputConnection#setSelection(int, int)}. - * - * @param start the character index where the selection should start. - * @param end the character index where the selection should end. - * @return Returns true on success, false on failure: either the input connection is no longer - * valid when setting the selection or when retrieving the text cache at that point, or - * invalid arguments were passed. - */ - public boolean setSelection(final int start, final int end) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - if (start < 0 || end < 0) { - return false; - } - mExpectedSelStart = start; - mExpectedSelEnd = end; - if (isConnected()) { - final boolean isIcValid = mIC.setSelection(start, end); - if (!isIcValid) { - return false; - } - } - return reloadTextCache(); - } - - public void commitCorrection(final CorrectionInfo correctionInfo) { - if (DEBUG_BATCH_NESTING) 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 (isConnected()) { - mIC.commitCorrection(correctionInfo); - } - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - } - - public void commitCompletion(final CompletionInfo completionInfo) { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - CharSequence text = completionInfo.getText(); - // text should never be null, but just in case, it's better to insert nothing than to crash - if (null == text) text = ""; - mCommittedTextBeforeComposingText.append(text); - mExpectedSelStart += text.length() - mComposingText.length(); - mExpectedSelEnd = mExpectedSelStart; - mComposingText.setLength(0); - if (isConnected()) { - mIC.commitCompletion(completionInfo); - } - if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - } - - @SuppressWarnings("unused") - @Nonnull - public NgramContext getNgramContextFromNthPreviousWord( - final SpacingAndPunctuations spacingAndPunctuations, final int n) { - mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return NgramContext.EMPTY_PREV_WORDS_INFO; - } - final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0); - if (DEBUG_PREVIOUS_TEXT && null != prev) { - final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1; - final String reference = prev.length() <= checkLength ? prev.toString() - : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); - // TODO: right now the following works because mComposingText holds the part of the - // composing text that is before the cursor, but this is very confusing. We should - // fix it. - 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 NgramContextUtils.getNgramContextFromNthPreviousWord( - prev, spacingAndPunctuations, n); - } - - private static boolean isPartOfCompositionForScript(final int codePoint, - final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) { - // We always consider word connectors part of compositions. - return spacingAndPunctuations.isWordConnector(codePoint) - // Otherwise, it's part of composition if it's part of script and not a separator. - || (!spacingAndPunctuations.isWordSeparator(codePoint) - && ScriptUtils.isLetterPartOfScript(codePoint, scriptId)); - } - - /** - * Returns the text surrounding the cursor. - * - * @param spacingAndPunctuations the rules for spacing and punctuation - * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_* - * @return a range containing the text surrounding the cursor - */ - public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations, - final int scriptId) { - mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return null; - } - final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection( - OPERATION_GET_WORD_RANGE_AT_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - NUM_CHARS_TO_GET_BEFORE_CURSOR, - InputConnection.GET_TEXT_WITH_STYLES); - final CharSequence after = getTextAfterCursorAndDetectLaggyConnection( - OPERATION_GET_WORD_RANGE_AT_CURSOR, - SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS, - NUM_CHARS_TO_GET_AFTER_CURSOR, - InputConnection.GET_TEXT_WITH_STYLES); - if (before == null || after == null) { - return null; - } - - // Going backward, find the first breaking point (separator) - int startIndexInBefore = before.length(); - while (startIndexInBefore > 0) { - final int codePoint = Character.codePointBefore(before, startIndexInBefore); - if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { - break; - } - --startIndexInBefore; - if (Character.isSupplementaryCodePoint(codePoint)) { - --startIndexInBefore; - } - } - - // Find last word separator after the cursor - int endIndexInAfter = -1; - while (++endIndexInAfter < after.length()) { - final int codePoint = Character.codePointAt(after, endIndexInAfter); - if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { - break; - } - if (Character.isSupplementaryCodePoint(codePoint)) { - ++endIndexInAfter; - } - } - - final boolean hasUrlSpans = - SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) - || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); - // We don't use TextUtils#concat because it copies all spans without respect to their - // nature. If the text includes a PARAGRAPH span and it has been split, then - // TextUtils#concat will crash when it tries to concat both sides of it. - return new TextRange( - SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), - startIndexInBefore, before.length() + endIndexInAfter, before.length(), - hasUrlSpans); - } - - 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; - } - final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); - int indexOfCodePointInJavaChars = textBeforeCursor.length(); - int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE - : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); - // Search for the first non word-connector char - if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { - indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); - consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE - : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); - } - return !(Constants.NOT_A_CODE == consideredCodePoint - || spacingAndPunctuations.isWordSeparator(consideredCodePoint) - || spacingAndPunctuations.isWordConnector(consideredCodePoint)); - } - - public boolean isCursorFollowedByWordCharacter( - final SpacingAndPunctuations spacingAndPunctuations) { - final CharSequence after = getTextAfterCursor(1, 0); - if (TextUtils.isEmpty(after)) { - return false; - } - final int codePointAfterCursor = Character.codePointAt(after, 0); - if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) - || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { - return false; - } - return true; - } - - public void removeTrailingSpace() { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - final int codePointBeforeCursor = getCodePointBeforeCursor(); - if (Constants.CODE_SPACE == codePointBeforeCursor) { - deleteTextBeforeCursor(1); - } - } - - public boolean sameAsTextBeforeCursor(final CharSequence text) { - final CharSequence beforeText = getTextBeforeCursor(text.length(), 0); - return TextUtils.equals(text, beforeText); - } - - 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(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 \"" - + spacingAndPunctuations.mSentenceSeparatorAndSpace - + "\" just before the cursor."); - return false; - } - // Double-space results in ". ". A backspace to cancel this should result in a single - // space in the text field, so we replace ". " with a single space. - deleteTextBeforeCursor(2); - final String singleSpace = " "; - commitText(singleSpace, 1); - return true; - } - - public boolean revertSwapPunctuation() { - if (DEBUG_BATCH_NESTING) checkBatchEdit(); - // Here we test whether we indeed have a space and something else before us. This should not - // be needed, but it's there just in case something went wrong. - final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); - // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to - // enter surrogate pairs this code will have been removed. - if (TextUtils.isEmpty(textBeforeCursor) - || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) { - // We may only come here if the application is changing the text while we are typing. - // This is quite a broken case, but not logically impossible, so we shouldn't crash, - // but some debugging log may be in order. - Log.d(TAG, "Tried to revert a swap of punctuation but we didn't " - + "find a space just before the cursor."); - return false; - } - deleteTextBeforeCursor(2); - final String text = " " + textBeforeCursor.subSequence(0, 1); - commitText(text, 1); - return true; - } - - /** - * Heuristic to determine if this is an expected update of the cursor. - * - * Sometimes updates to the cursor position are late because of their asynchronous nature. - * This method tries to determine if this update is one, based on the values of the cursor - * position in the update, and the currently expected position of the cursor according to - * LatinIME's internal accounting. If this is not a belated expected update, then it should - * mean that the user moved the cursor explicitly. - * This is quite robust, but of course it's not perfect. In particular, it will fail in the - * case we get an update A, the user types in N characters so as to move the cursor to A+N but - * we don't get those, and then the user places the cursor between A and A+N, and we get only - * this update and not the ones in-between. This is almost impossible to achieve even trying - * very very hard. - * - * @param oldSelStart The value of the old selection in the update. - * @param newSelStart The value of the new selection in the update. - * @param oldSelEnd The value of the old selection end in the update. - * @param newSelEnd The value of the new selection end in the update. - * @return whether this is a belated expected update or not. - */ - public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, - final int oldSelEnd, final int newSelEnd) { - // This update is "belated" if we are expecting it. That is, mExpectedSelStart and - // mExpectedSelEnd match the new values that the TextView is updating TO. - if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; - // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old - // values, and one of newSelStart or newSelEnd is updated to a different value. In this - // case, it is likely that something other than the IME has moved the selection endpoint - // to the new value. - if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd - && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; - // If neither of the above two cases hold, then the system may be having trouble keeping up - // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart - // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then - // assume a belated update. - return (newSelStart == newSelEnd) - && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 - && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; - } - - /** - * Looks at the text just before the cursor to find out if it looks like a URL. - * - * The weakest point here is, if we don't have enough text bufferized, we may fail to realize - * we are in URL situation, but other places in this class have the same limitation and it - * does not matter too much in the practice. - */ - public boolean textBeforeCursorLooksLikeURL() { - return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); - } - - /** - * Looks at the text just before the cursor to find out if we are inside a double quote. - * - * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. - * However this won't be a concrete problem in most situations, as the cache is almost always - * long enough for this use. - */ - public boolean isInsideDoubleQuoteOrAfterDigit() { - return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); - } - - /** - * Try to get the text from the editor to expose lies the framework may have been - * 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); - 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(); - if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE - && (textLength > mExpectedSelStart - || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { - // It should not be possible to have only one of those variables be - // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized - // (simple cursor, no selection) or there is no cursor/we don't know its pos - final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; - mExpectedSelStart = textLength; - // We can't figure out the value of mLastSelectionEnd :( - // But at least if it's smaller than mLastSelectionStart something is wrong, - // and if they used to be equal we also don't want to make it look like there is a - // selection. - if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { - mExpectedSelEnd = mExpectedSelStart; - } - } - } - } - - @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; - } - - public int getExpectedSelectionEnd() { - return mExpectedSelEnd; - } - - /** - * @return whether there is a selection currently active. - */ - public boolean hasSelection() { - return mExpectedSelEnd != mExpectedSelStart; - } - - public boolean isCursorPositionKnown() { - return INVALID_CURSOR_POSITION != mExpectedSelStart; - } - - /** - * Work around a bug that was present before Jelly Bean upon rotation. - * - * Before Jelly Bean, there is a bug where setComposingRegion and other committing - * functions on the input connection get ignored until the cursor moves. This method works - * around the bug by wiggling the cursor first, which reactivates the connection and has - * the subsequent methods work, then restoring it to its original position. - * - * On platforms on which this method is not present, this is a no-op. - */ - public void maybeMoveTheCursorAroundAndRestoreToWorkaroundABug() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - if (mExpectedSelStart > 0) { - mIC.setSelection(mExpectedSelStart - 1, mExpectedSelStart - 1); - } else { - mIC.setSelection(mExpectedSelStart + 1, mExpectedSelStart + 1); - } - mIC.setSelection(mExpectedSelStart, mExpectedSelEnd); - } - } - - /** - * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. - * @param enableMonitor {@code true} to request the editor to call back the method whenever the - * cursor/anchor position is changed. - * @param requestImmediateCallback {@code true} to request the editor to call back the method - * as soon as possible to notify the current cursor/anchor position to the input method. - * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which - * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which - * prevents the application from fulfilling the request. (TODO: Improve the API when it turns - * out that we actually need more detailed error codes) - */ - public boolean requestCursorUpdates(final boolean enableMonitor, - final boolean requestImmediateCallback) { - mIC = mParent.getCurrentInputConnection(); - if (!isConnected()) { - return false; - } - return InputConnectionCompatUtils.requestCursorUpdates( - mIC, enableMonitor, requestImmediateCallback); - } -} |