Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.graphics.Color;
     20 import android.inputmethodservice.InputMethodService;
     21 import android.os.Build;
     22 import android.text.SpannableStringBuilder;
     23 import android.text.Spanned;
     24 import android.text.TextUtils;
     25 import android.text.style.BackgroundColorSpan;
     26 import android.util.Log;
     27 import android.view.KeyEvent;
     28 import android.view.inputmethod.CompletionInfo;
     29 import android.view.inputmethod.CorrectionInfo;
     30 import android.view.inputmethod.ExtractedText;
     31 import android.view.inputmethod.ExtractedTextRequest;
     32 import android.view.inputmethod.InputConnection;
     33 import android.view.inputmethod.InputMethodManager;
     34 
     35 import com.android.inputmethod.compat.InputConnectionCompatUtils;
     36 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
     37 import com.android.inputmethod.latin.utils.CapsModeUtils;
     38 import com.android.inputmethod.latin.utils.DebugLogUtils;
     39 import com.android.inputmethod.latin.utils.PrevWordsInfoUtils;
     40 import com.android.inputmethod.latin.utils.ScriptUtils;
     41 import com.android.inputmethod.latin.utils.SpannableStringUtils;
     42 import com.android.inputmethod.latin.utils.StringUtils;
     43 import com.android.inputmethod.latin.utils.TextRange;
     44 
     45 import java.util.Arrays;
     46 
     47 /**
     48  * Enrichment class for InputConnection to simplify interaction and add functionality.
     49  *
     50  * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
     51  * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
     52  * all the time to find out what text is in the buffer, when we need it to determine caps mode
     53  * for example.
     54  */
     55 public final class RichInputConnection {
     56     private static final String TAG = RichInputConnection.class.getSimpleName();
     57     private static final boolean DBG = false;
     58     private static final boolean DEBUG_PREVIOUS_TEXT = false;
     59     private static final boolean DEBUG_BATCH_NESTING = false;
     60     // Provision for long words and separators between the words.
     61     private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH
     62             * (Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1) /* words */
     63             + Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM /* separators */;
     64     private static final int INVALID_CURSOR_POSITION = -1;
     65 
     66     /**
     67      * This variable contains an expected value for the selection start position. This is where the
     68      * cursor or selection start may end up after all the keyboard-triggered updates have passed. We
     69      * keep this to compare it to the actual selection start to guess whether the move was caused by
     70      * a keyboard command or not.
     71      * It's not really the selection start position: the selection start may not be there yet, and
     72      * in some cases, it may never arrive there.
     73      */
     74     private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points
     75     /**
     76      * The expected selection end.  Only differs from mExpectedSelStart if a non-empty selection is
     77      * expected.  The same caveats as mExpectedSelStart apply.
     78      */
     79     private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points
     80     /**
     81      * This contains the committed text immediately preceding the cursor and the composing
     82      * text if any. It is refreshed when the cursor moves by calling upon the TextView.
     83      */
     84     private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
     85     /**
     86      * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
     87      */
     88     private final StringBuilder mComposingText = new StringBuilder();
     89 
     90     /**
     91      * This variable is a temporary object used in
     92      * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation.
     93      */
     94     private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
     95     /**
     96      * This variable is used to track whether the last committed text had the background color or
     97      * not.
     98      * TODO: Omit this flag if possible.
     99      */
    100     private boolean mLastCommittedTextHasBackgroundColor = false;
    101 
    102     private final InputMethodService mParent;
    103     InputConnection mIC;
    104     int mNestLevel;
    105     public RichInputConnection(final InputMethodService parent) {
    106         mParent = parent;
    107         mIC = null;
    108         mNestLevel = 0;
    109     }
    110 
    111     private void checkConsistencyForDebug() {
    112         final ExtractedTextRequest r = new ExtractedTextRequest();
    113         r.hintMaxChars = 0;
    114         r.hintMaxLines = 0;
    115         r.token = 1;
    116         r.flags = 0;
    117         final ExtractedText et = mIC.getExtractedText(r, 0);
    118         final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
    119                 0);
    120         final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText)
    121                 .append(mComposingText);
    122         if (null == et || null == beforeCursor) return;
    123         final int actualLength = Math.min(beforeCursor.length(), internal.length());
    124         if (internal.length() > actualLength) {
    125             internal.delete(0, internal.length() - actualLength);
    126         }
    127         final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
    128                 : beforeCursor.subSequence(beforeCursor.length() - actualLength,
    129                         beforeCursor.length()).toString();
    130         if (et.selectionStart != mExpectedSelStart
    131                 || !(reference.equals(internal.toString()))) {
    132             final String context = "Expected selection start = " + mExpectedSelStart
    133                     + "\nActual selection start = " + et.selectionStart
    134                     + "\nExpected text = " + internal.length() + " " + internal
    135                     + "\nActual text = " + reference.length() + " " + reference;
    136             ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
    137         } else {
    138             Log.e(TAG, DebugLogUtils.getStackTrace(2));
    139             Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart);
    140         }
    141     }
    142 
    143     public void beginBatchEdit() {
    144         if (++mNestLevel == 1) {
    145             mIC = mParent.getCurrentInputConnection();
    146             if (null != mIC) {
    147                 mIC.beginBatchEdit();
    148             }
    149         } else {
    150             if (DBG) {
    151                 throw new RuntimeException("Nest level too deep");
    152             } else {
    153                 Log.e(TAG, "Nest level too deep : " + mNestLevel);
    154             }
    155         }
    156         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    157         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    158     }
    159 
    160     public void endBatchEdit() {
    161         if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
    162         if (--mNestLevel == 0 && null != mIC) {
    163             mIC.endBatchEdit();
    164         }
    165         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    166     }
    167 
    168     /**
    169      * Reset the cached text and retrieve it again from the editor.
    170      *
    171      * This should be called when the cursor moved. It's possible that we can't connect to
    172      * the application when doing this; notably, this happens sometimes during rotation, probably
    173      * because of a race condition in the framework. In this case, we just can't retrieve the
    174      * data, so we empty the cache and note that we don't know the new cursor position, and we
    175      * return false so that the caller knows about this and can retry later.
    176      *
    177      * @param newSelStart the new position of the selection start, as received from the system.
    178      * @param newSelEnd the new position of the selection end, as received from the system.
    179      * @param shouldFinishComposition whether we should finish the composition in progress.
    180      * @return true if we were able to connect to the editor successfully, false otherwise. When
    181      *   this method returns false, the caches could not be correctly refreshed so they were only
    182      *   reset: the caller should try again later to return to normal operation.
    183      */
    184     public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart,
    185             final int newSelEnd, final boolean shouldFinishComposition) {
    186         mExpectedSelStart = newSelStart;
    187         mExpectedSelEnd = newSelEnd;
    188         mComposingText.setLength(0);
    189         final boolean didReloadTextSuccessfully = reloadTextCache();
    190         if (!didReloadTextSuccessfully) {
    191             Log.d(TAG, "Will try to retrieve text later.");
    192             return false;
    193         }
    194         if (null != mIC && shouldFinishComposition) {
    195             mIC.finishComposingText();
    196         }
    197         return true;
    198     }
    199 
    200     /**
    201      * Reload the cached text from the InputConnection.
    202      *
    203      * @return true if successful
    204      */
    205     private boolean reloadTextCache() {
    206         mCommittedTextBeforeComposingText.setLength(0);
    207         mIC = mParent.getCurrentInputConnection();
    208         // Call upon the inputconnection directly since our own method is using the cache, and
    209         // we want to refresh it.
    210         final CharSequence textBeforeCursor = null == mIC ? null :
    211                 mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
    212         if (null == textBeforeCursor) {
    213             // For some reason the app thinks we are not connected to it. This looks like a
    214             // framework bug... Fall back to ground state and return false.
    215             mExpectedSelStart = INVALID_CURSOR_POSITION;
    216             mExpectedSelEnd = INVALID_CURSOR_POSITION;
    217             Log.e(TAG, "Unable to connect to the editor to retrieve text.");
    218             return false;
    219         }
    220         mCommittedTextBeforeComposingText.append(textBeforeCursor);
    221         return true;
    222     }
    223 
    224     private void checkBatchEdit() {
    225         if (mNestLevel != 1) {
    226             // TODO: exception instead
    227             Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
    228             Log.e(TAG, DebugLogUtils.getStackTrace(4));
    229         }
    230     }
    231 
    232     public void finishComposingText() {
    233         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    234         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    235         // TODO: this is not correct! The cursor is not necessarily after the composing text.
    236         // In the practice right now this is only called when input ends so it will be reset so
    237         // it works, but it's wrong and should be fixed.
    238         mCommittedTextBeforeComposingText.append(mComposingText);
    239         mComposingText.setLength(0);
    240         // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed.
    241         mLastCommittedTextHasBackgroundColor = false;
    242         if (null != mIC) {
    243             mIC.finishComposingText();
    244         }
    245     }
    246 
    247     /**
    248      * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}.
    249      * @param text The text to commit. This may include styles.
    250      * See {@link InputConnection#commitText(CharSequence, int)}.
    251      * @param newCursorPosition The new cursor position around the text.
    252      * See {@link InputConnection#commitText(CharSequence, int)}.
    253      */
    254     public void commitText(final CharSequence text, final int newCursorPosition) {
    255         commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT, text.length());
    256     }
    257 
    258     /**
    259      * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color.
    260      * @param text The text to commit. This may include styles.
    261      * See {@link InputConnection#commitText(CharSequence, int)}.
    262      * @param newCursorPosition The new cursor position around the text.
    263      * See {@link InputConnection#commitText(CharSequence, int)}.
    264      * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable
    265      * the background color. Note that this method specifies {@link BackgroundColorSpan} with
    266      * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until
    267      * {@link #finishComposingText()} is called.
    268      * @param coloredTextLength the length of text, in Java chars, which should be rendered with
    269      * the given background color.
    270      */
    271     public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition,
    272             final int color, final int coloredTextLength) {
    273         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    274         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    275         mCommittedTextBeforeComposingText.append(text);
    276         // TODO: the following is exceedingly error-prone. Right now when the cursor is in the
    277         // middle of the composing word mComposingText only holds the part of the composing text
    278         // that is before the cursor, so this actually works, but it's terribly confusing. Fix this.
    279         mExpectedSelStart += text.length() - mComposingText.length();
    280         mExpectedSelEnd = mExpectedSelStart;
    281         mComposingText.setLength(0);
    282         mLastCommittedTextHasBackgroundColor = false;
    283         if (null != mIC) {
    284             if (color == Color.TRANSPARENT) {
    285                 mIC.commitText(text, newCursorPosition);
    286             } else {
    287                 mTempObjectForCommitText.clear();
    288                 mTempObjectForCommitText.append(text);
    289                 final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color);
    290                 final int spanLength = Math.min(coloredTextLength, text.length());
    291                 mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, spanLength,
    292                         Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    293                 mIC.commitText(mTempObjectForCommitText, newCursorPosition);
    294                 mLastCommittedTextHasBackgroundColor = true;
    295             }
    296         }
    297     }
    298 
    299     /**
    300      * Removes the background color from the highlighted text if necessary. Should be called while
    301      * there is no on-going composing text.
    302      *
    303      * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}.
    304      * Be careful of any unexpected side effects.</p>
    305      */
    306     public void removeBackgroundColorFromHighlightedTextIfNecessary() {
    307         // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this
    308         // flag if everything works fine without this condition.
    309         if (!mLastCommittedTextHasBackgroundColor) {
    310             return;
    311         }
    312         if (mComposingText.length() > 0) {
    313             Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " +
    314                     "empty. mComposingText=" + mComposingText);
    315             return;
    316         }
    317         finishComposingText();
    318     }
    319 
    320     public CharSequence getSelectedText(final int flags) {
    321         return (null == mIC) ? null : mIC.getSelectedText(flags);
    322     }
    323 
    324     public boolean canDeleteCharacters() {
    325         return mExpectedSelStart > 0;
    326     }
    327 
    328     /**
    329      * Gets the caps modes we should be in after this specific string.
    330      *
    331      * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
    332      * This method also supports faking an additional space after the string passed in argument,
    333      * to support cases where a space will be added automatically, like in phantom space
    334      * state for example.
    335      * Note that for English, we are using American typography rules (which are not specific to
    336      * American English, it's just the most common set of rules for English).
    337      *
    338      * @param inputType a mask of the caps modes to test for.
    339      * @param spacingAndPunctuations the values of the settings to use for locale and separators.
    340      * @param hasSpaceBefore if we should consider there should be a space after the string.
    341      * @return the caps modes that should be on as a set of bits
    342      */
    343     public int getCursorCapsMode(final int inputType,
    344             final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
    345         mIC = mParent.getCurrentInputConnection();
    346         if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
    347         if (!TextUtils.isEmpty(mComposingText)) {
    348             if (hasSpaceBefore) {
    349                 // If we have some composing text and a space before, then we should have
    350                 // MODE_CHARACTERS and MODE_WORDS on.
    351                 return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
    352             } else {
    353                 // We have some composing text - we should be in MODE_CHARACTERS only.
    354                 return TextUtils.CAP_MODE_CHARACTERS & inputType;
    355             }
    356         }
    357         // TODO: this will generally work, but there may be cases where the buffer contains SOME
    358         // information but not enough to determine the caps mode accurately. This may happen after
    359         // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
    360         // getCapsMode should be updated to be able to return a "not enough info" result so that
    361         // we can get more context only when needed.
    362         if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) {
    363             if (!reloadTextCache()) {
    364                 Log.w(TAG, "Unable to connect to the editor. "
    365                         + "Setting caps mode without knowing text.");
    366             }
    367         }
    368         // This never calls InputConnection#getCapsMode - in fact, it's a static method that
    369         // never blocks or initiates IPC.
    370         return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
    371                 spacingAndPunctuations, hasSpaceBefore);
    372     }
    373 
    374     public int getCodePointBeforeCursor() {
    375         final int length = mCommittedTextBeforeComposingText.length();
    376         if (length < 1) return Constants.NOT_A_CODE;
    377         return Character.codePointBefore(mCommittedTextBeforeComposingText, length);
    378     }
    379 
    380     public CharSequence getTextBeforeCursor(final int n, final int flags) {
    381         final int cachedLength =
    382                 mCommittedTextBeforeComposingText.length() + mComposingText.length();
    383         // If we have enough characters to satisfy the request, or if we have all characters in
    384         // the text field, then we can return the cached version right away.
    385         // However, if we don't have an expected cursor position, then we should always
    386         // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to
    387         // test for this explicitly)
    388         if (INVALID_CURSOR_POSITION != mExpectedSelStart
    389                 && (cachedLength >= n || cachedLength >= mExpectedSelStart)) {
    390             final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
    391             // We call #toString() here to create a temporary object.
    392             // In some situations, this method is called on a worker thread, and it's possible
    393             // the main thread touches the contents of mComposingText while this worker thread
    394             // is suspended, because mComposingText is a StringBuilder. This may lead to crashes,
    395             // so we call #toString() on it. That will result in the return value being strictly
    396             // speaking wrong, but since this is used for basing bigram probability off, and
    397             // it's only going to matter for one getSuggestions call, it's fine in the practice.
    398             s.append(mComposingText.toString());
    399             if (s.length() > n) {
    400                 s.delete(0, s.length() - n);
    401             }
    402             return s;
    403         }
    404         mIC = mParent.getCurrentInputConnection();
    405         return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags);
    406     }
    407 
    408     public CharSequence getTextAfterCursor(final int n, final int flags) {
    409         mIC = mParent.getCurrentInputConnection();
    410         return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags);
    411     }
    412 
    413     public void deleteSurroundingText(final int beforeLength, final int afterLength) {
    414         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    415         // TODO: the following is incorrect if the cursor is not immediately after the composition.
    416         // Right now we never come here in this case because we reset the composing state before we
    417         // come here in this case, but we need to fix this.
    418         final int remainingChars = mComposingText.length() - beforeLength;
    419         if (remainingChars >= 0) {
    420             mComposingText.setLength(remainingChars);
    421         } else {
    422             mComposingText.setLength(0);
    423             // Never cut under 0
    424             final int len = Math.max(mCommittedTextBeforeComposingText.length()
    425                     + remainingChars, 0);
    426             mCommittedTextBeforeComposingText.setLength(len);
    427         }
    428         if (mExpectedSelStart > beforeLength) {
    429             mExpectedSelStart -= beforeLength;
    430             mExpectedSelEnd -= beforeLength;
    431         } else {
    432             // There are fewer characters before the cursor in the buffer than we are being asked to
    433             // delete. Only delete what is there, and update the end with the amount deleted.
    434             mExpectedSelEnd -= mExpectedSelStart;
    435             mExpectedSelStart = 0;
    436         }
    437         if (null != mIC) {
    438             mIC.deleteSurroundingText(beforeLength, afterLength);
    439         }
    440         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    441     }
    442 
    443     public void performEditorAction(final int actionId) {
    444         mIC = mParent.getCurrentInputConnection();
    445         if (null != mIC) {
    446             mIC.performEditorAction(actionId);
    447         }
    448     }
    449 
    450     public void sendKeyEvent(final KeyEvent keyEvent) {
    451         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    452         if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
    453             if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    454             // This method is only called for enter or backspace when speaking to old applications
    455             // (target SDK <= 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)), or for digits.
    456             // When talking to new applications we never use this method because it's inherently
    457             // racy and has unpredictable results, but for backward compatibility we continue
    458             // sending the key events for only Enter and Backspace because some applications
    459             // mistakenly catch them to do some stuff.
    460             switch (keyEvent.getKeyCode()) {
    461             case KeyEvent.KEYCODE_ENTER:
    462                 mCommittedTextBeforeComposingText.append("\n");
    463                 mExpectedSelStart += 1;
    464                 mExpectedSelEnd = mExpectedSelStart;
    465                 break;
    466             case KeyEvent.KEYCODE_DEL:
    467                 if (0 == mComposingText.length()) {
    468                     if (mCommittedTextBeforeComposingText.length() > 0) {
    469                         mCommittedTextBeforeComposingText.delete(
    470                                 mCommittedTextBeforeComposingText.length() - 1,
    471                                 mCommittedTextBeforeComposingText.length());
    472                     }
    473                 } else {
    474                     mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
    475                 }
    476                 if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) {
    477                     // TODO: Handle surrogate pairs.
    478                     mExpectedSelStart -= 1;
    479                 }
    480                 mExpectedSelEnd = mExpectedSelStart;
    481                 break;
    482             case KeyEvent.KEYCODE_UNKNOWN:
    483                 if (null != keyEvent.getCharacters()) {
    484                     mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
    485                     mExpectedSelStart += keyEvent.getCharacters().length();
    486                     mExpectedSelEnd = mExpectedSelStart;
    487                 }
    488                 break;
    489             default:
    490                 final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar());
    491                 mCommittedTextBeforeComposingText.append(text);
    492                 mExpectedSelStart += text.length();
    493                 mExpectedSelEnd = mExpectedSelStart;
    494                 break;
    495             }
    496         }
    497         if (null != mIC) {
    498             mIC.sendKeyEvent(keyEvent);
    499         }
    500     }
    501 
    502     public void setComposingRegion(final int start, final int end) {
    503         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    504         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    505         final CharSequence textBeforeCursor =
    506                 getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0);
    507         mCommittedTextBeforeComposingText.setLength(0);
    508         if (!TextUtils.isEmpty(textBeforeCursor)) {
    509             // The cursor is not necessarily at the end of the composing text, but we have its
    510             // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start
    511             // of the text, so we should use mExpectedSelStart. In other words, the composing
    512             // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor
    513             final int indexOfStartOfComposingText =
    514                     Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0);
    515             mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText,
    516                     textBeforeCursor.length()));
    517             mCommittedTextBeforeComposingText.append(
    518                     textBeforeCursor.subSequence(0, indexOfStartOfComposingText));
    519         }
    520         if (null != mIC) {
    521             mIC.setComposingRegion(start, end);
    522         }
    523     }
    524 
    525     public void setComposingText(final CharSequence text, final int newCursorPosition) {
    526         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    527         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    528         mExpectedSelStart += text.length() - mComposingText.length();
    529         mExpectedSelEnd = mExpectedSelStart;
    530         mComposingText.setLength(0);
    531         mComposingText.append(text);
    532         // TODO: support values of newCursorPosition != 1. At this time, this is never called with
    533         // newCursorPosition != 1.
    534         if (null != mIC) {
    535             mIC.setComposingText(text, newCursorPosition);
    536         }
    537         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    538     }
    539 
    540     /**
    541      * Set the selection of the text editor.
    542      *
    543      * Calls through to {@link InputConnection#setSelection(int, int)}.
    544      *
    545      * @param start the character index where the selection should start.
    546      * @param end the character index where the selection should end.
    547      * @return Returns true on success, false on failure: either the input connection is no longer
    548      * valid when setting the selection or when retrieving the text cache at that point, or
    549      * invalid arguments were passed.
    550      */
    551     public boolean setSelection(final int start, final int end) {
    552         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    553         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    554         if (start < 0 || end < 0) {
    555             return false;
    556         }
    557         mExpectedSelStart = start;
    558         mExpectedSelEnd = end;
    559         if (null != mIC) {
    560             final boolean isIcValid = mIC.setSelection(start, end);
    561             if (!isIcValid) {
    562                 return false;
    563             }
    564         }
    565         return reloadTextCache();
    566     }
    567 
    568     public void commitCorrection(final CorrectionInfo correctionInfo) {
    569         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    570         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    571         // This has no effect on the text field and does not change its content. It only makes
    572         // TextView flash the text for a second based on indices contained in the argument.
    573         if (null != mIC) {
    574             mIC.commitCorrection(correctionInfo);
    575         }
    576         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    577     }
    578 
    579     public void commitCompletion(final CompletionInfo completionInfo) {
    580         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    581         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    582         CharSequence text = completionInfo.getText();
    583         // text should never be null, but just in case, it's better to insert nothing than to crash
    584         if (null == text) text = "";
    585         mCommittedTextBeforeComposingText.append(text);
    586         mExpectedSelStart += text.length() - mComposingText.length();
    587         mExpectedSelEnd = mExpectedSelStart;
    588         mComposingText.setLength(0);
    589         if (null != mIC) {
    590             mIC.commitCompletion(completionInfo);
    591         }
    592         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    593     }
    594 
    595     @SuppressWarnings("unused")
    596     public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(
    597             final SpacingAndPunctuations spacingAndPunctuations, final int n) {
    598         mIC = mParent.getCurrentInputConnection();
    599         if (null == mIC) {
    600             return PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
    601         }
    602         final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
    603         if (DEBUG_PREVIOUS_TEXT && null != prev) {
    604             final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
    605             final String reference = prev.length() <= checkLength ? prev.toString()
    606                     : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
    607             // TODO: right now the following works because mComposingText holds the part of the
    608             // composing text that is before the cursor, but this is very confusing. We should
    609             // fix it.
    610             final StringBuilder internal = new StringBuilder()
    611                     .append(mCommittedTextBeforeComposingText).append(mComposingText);
    612             if (internal.length() > checkLength) {
    613                 internal.delete(0, internal.length() - checkLength);
    614                 if (!(reference.equals(internal.toString()))) {
    615                     final String context =
    616                             "Expected text = " + internal + "\nActual text = " + reference;
    617                     ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
    618                 }
    619             }
    620         }
    621         return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord(
    622                 prev, spacingAndPunctuations, n);
    623     }
    624 
    625     private static boolean isSeparator(final int code, final int[] sortedSeparators) {
    626         return Arrays.binarySearch(sortedSeparators, code) >= 0;
    627     }
    628 
    629     private static boolean isPartOfCompositionForScript(final int codePoint,
    630             final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) {
    631         // We always consider word connectors part of compositions.
    632         return spacingAndPunctuations.isWordConnector(codePoint)
    633                 // Otherwise, it's part of composition if it's part of script and not a separator.
    634                 || (!spacingAndPunctuations.isWordSeparator(codePoint)
    635                         && ScriptUtils.isLetterPartOfScript(codePoint, scriptId));
    636     }
    637 
    638     /**
    639      * Returns the text surrounding the cursor.
    640      *
    641      * @param spacingAndPunctuations the rules for spacing and punctuation
    642      * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_*
    643      * @return a range containing the text surrounding the cursor
    644      */
    645     public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations,
    646             final int scriptId) {
    647         mIC = mParent.getCurrentInputConnection();
    648         if (mIC == null) {
    649             return null;
    650         }
    651         final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
    652                 InputConnection.GET_TEXT_WITH_STYLES);
    653         final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
    654                 InputConnection.GET_TEXT_WITH_STYLES);
    655         if (before == null || after == null) {
    656             return null;
    657         }
    658 
    659         // Going backward, find the first breaking point (separator)
    660         int startIndexInBefore = before.length();
    661         while (startIndexInBefore > 0) {
    662             final int codePoint = Character.codePointBefore(before, startIndexInBefore);
    663             if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) {
    664                 break;
    665             }
    666             --startIndexInBefore;
    667             if (Character.isSupplementaryCodePoint(codePoint)) {
    668                 --startIndexInBefore;
    669             }
    670         }
    671 
    672         // Find last word separator after the cursor
    673         int endIndexInAfter = -1;
    674         while (++endIndexInAfter < after.length()) {
    675             final int codePoint = Character.codePointAt(after, endIndexInAfter);
    676             if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) {
    677                 break;
    678             }
    679             if (Character.isSupplementaryCodePoint(codePoint)) {
    680                 ++endIndexInAfter;
    681             }
    682         }
    683 
    684         final boolean hasUrlSpans =
    685                 SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length())
    686                 || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter);
    687         // We don't use TextUtils#concat because it copies all spans without respect to their
    688         // nature. If the text includes a PARAGRAPH span and it has been split, then
    689         // TextUtils#concat will crash when it tries to concat both sides of it.
    690         return new TextRange(
    691                 SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after),
    692                         startIndexInBefore, before.length() + endIndexInAfter, before.length(),
    693                         hasUrlSpans);
    694     }
    695 
    696     public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) {
    697         if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
    698             // If what's after the cursor is a word character, then we're touching a word.
    699             return true;
    700         }
    701         final String textBeforeCursor = mCommittedTextBeforeComposingText.toString();
    702         int indexOfCodePointInJavaChars = textBeforeCursor.length();
    703         int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
    704                 : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
    705         // Search for the first non word-connector char
    706         if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) {
    707             indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint);
    708             consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE
    709                     : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars);
    710         }
    711         return !(Constants.NOT_A_CODE == consideredCodePoint
    712                 || spacingAndPunctuations.isWordSeparator(consideredCodePoint)
    713                 || spacingAndPunctuations.isWordConnector(consideredCodePoint));
    714     }
    715 
    716     public boolean isCursorFollowedByWordCharacter(
    717             final SpacingAndPunctuations spacingAndPunctuations) {
    718         final CharSequence after = getTextAfterCursor(1, 0);
    719         if (TextUtils.isEmpty(after)) {
    720             return false;
    721         }
    722         final int codePointAfterCursor = Character.codePointAt(after, 0);
    723         if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor)
    724                 || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) {
    725             return false;
    726         }
    727         return true;
    728     }
    729 
    730     public void removeTrailingSpace() {
    731         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    732         final int codePointBeforeCursor = getCodePointBeforeCursor();
    733         if (Constants.CODE_SPACE == codePointBeforeCursor) {
    734             deleteSurroundingText(1, 0);
    735         }
    736     }
    737 
    738     public boolean sameAsTextBeforeCursor(final CharSequence text) {
    739         final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
    740         return TextUtils.equals(text, beforeText);
    741     }
    742 
    743     public boolean revertDoubleSpacePeriod() {
    744         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    745         // Here we test whether we indeed have a period and a space before us. This should not
    746         // be needed, but it's there just in case something went wrong.
    747         final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
    748         if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) {
    749             // Theoretically we should not be coming here if there isn't ". " before the
    750             // cursor, but the application may be changing the text while we are typing, so
    751             // anything goes. We should not crash.
    752             Log.d(TAG, "Tried to revert double-space combo but we didn't find "
    753                     + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor.");
    754             return false;
    755         }
    756         // Double-space results in ". ". A backspace to cancel this should result in a single
    757         // space in the text field, so we replace ". " with a single space.
    758         deleteSurroundingText(2, 0);
    759         final String singleSpace = " ";
    760         commitText(singleSpace, 1);
    761         return true;
    762     }
    763 
    764     public boolean revertSwapPunctuation() {
    765         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    766         // Here we test whether we indeed have a space and something else before us. This should not
    767         // be needed, but it's there just in case something went wrong.
    768         final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
    769         // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
    770         // enter surrogate pairs this code will have been removed.
    771         if (TextUtils.isEmpty(textBeforeCursor)
    772                 || (Constants.CODE_SPACE != textBeforeCursor.charAt(1))) {
    773             // We may only come here if the application is changing the text while we are typing.
    774             // This is quite a broken case, but not logically impossible, so we shouldn't crash,
    775             // but some debugging log may be in order.
    776             Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
    777                     + "find a space just before the cursor.");
    778             return false;
    779         }
    780         deleteSurroundingText(2, 0);
    781         final String text = " " + textBeforeCursor.subSequence(0, 1);
    782         commitText(text, 1);
    783         return true;
    784     }
    785 
    786     /**
    787      * Heuristic to determine if this is an expected update of the cursor.
    788      *
    789      * Sometimes updates to the cursor position are late because of their asynchronous nature.
    790      * This method tries to determine if this update is one, based on the values of the cursor
    791      * position in the update, and the currently expected position of the cursor according to
    792      * LatinIME's internal accounting. If this is not a belated expected update, then it should
    793      * mean that the user moved the cursor explicitly.
    794      * This is quite robust, but of course it's not perfect. In particular, it will fail in the
    795      * case we get an update A, the user types in N characters so as to move the cursor to A+N but
    796      * we don't get those, and then the user places the cursor between A and A+N, and we get only
    797      * this update and not the ones in-between. This is almost impossible to achieve even trying
    798      * very very hard.
    799      *
    800      * @param oldSelStart The value of the old selection in the update.
    801      * @param newSelStart The value of the new selection in the update.
    802      * @param oldSelEnd The value of the old selection end in the update.
    803      * @param newSelEnd The value of the new selection end in the update.
    804      * @return whether this is a belated expected update or not.
    805      */
    806     public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart,
    807             final int oldSelEnd, final int newSelEnd) {
    808         // This update is "belated" if we are expecting it. That is, mExpectedSelStart and
    809         // mExpectedSelEnd match the new values that the TextView is updating TO.
    810         if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true;
    811         // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old
    812         // values, and one of newSelStart or newSelEnd is updated to a different value. In this
    813         // case, it is likely that something other than the IME has moved the selection endpoint
    814         // to the new value.
    815         if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
    816                 && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
    817         // If neither of the above two cases hold, then the system may be having trouble keeping up
    818         // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
    819         // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then
    820         // assume a belated update.
    821         return (newSelStart == newSelEnd)
    822                 && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0
    823                 && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0;
    824     }
    825 
    826     /**
    827      * Looks at the text just before the cursor to find out if it looks like a URL.
    828      *
    829      * The weakest point here is, if we don't have enough text bufferized, we may fail to realize
    830      * we are in URL situation, but other places in this class have the same limitation and it
    831      * does not matter too much in the practice.
    832      */
    833     public boolean textBeforeCursorLooksLikeURL() {
    834         return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
    835     }
    836 
    837     /**
    838      * Looks at the text just before the cursor to find out if we are inside a double quote.
    839      *
    840      * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached.
    841      * However this won't be a concrete problem in most situations, as the cache is almost always
    842      * long enough for this use.
    843      */
    844     public boolean isInsideDoubleQuoteOrAfterDigit() {
    845         return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText);
    846     }
    847 
    848     /**
    849      * Try to get the text from the editor to expose lies the framework may have been
    850      * telling us. Concretely, when the device rotates, the frameworks tells us about where the
    851      * cursor used to be initially in the editor at the time it first received the focus; this
    852      * may be completely different from the place it is upon rotation. Since we don't have any
    853      * means to get the real value, try at least to ask the text view for some characters and
    854      * detect the most damaging cases: when the cursor position is declared to be much smaller
    855      * than it really is.
    856      */
    857     public void tryFixLyingCursorPosition() {
    858         final CharSequence textBeforeCursor = getTextBeforeCursor(
    859                 Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
    860         if (null == textBeforeCursor) {
    861             mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
    862         } else {
    863             final int textLength = textBeforeCursor.length();
    864             if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
    865                     && (textLength > mExpectedSelStart
    866                             ||  mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
    867                 // It should not be possible to have only one of those variables be
    868                 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
    869                 // (simple cursor, no selection) or there is no cursor/we don't know its pos
    870                 final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd;
    871                 mExpectedSelStart = textLength;
    872                 // We can't figure out the value of mLastSelectionEnd :(
    873                 // But at least if it's smaller than mLastSelectionStart something is wrong,
    874                 // and if they used to be equal we also don't want to make it look like there is a
    875                 // selection.
    876                 if (wasEqual || mExpectedSelStart > mExpectedSelEnd) {
    877                     mExpectedSelEnd = mExpectedSelStart;
    878                 }
    879             }
    880         }
    881     }
    882 
    883     public int getExpectedSelectionStart() {
    884         return mExpectedSelStart;
    885     }
    886 
    887     public int getExpectedSelectionEnd() {
    888         return mExpectedSelEnd;
    889     }
    890 
    891     /**
    892      * @return whether there is a selection currently active.
    893      */
    894     public boolean hasSelection() {
    895         return mExpectedSelEnd != mExpectedSelStart;
    896     }
    897 
    898     public boolean isCursorPositionKnown() {
    899         return INVALID_CURSOR_POSITION != mExpectedSelStart;
    900     }
    901 
    902     /**
    903      * Work around a bug that was present before Jelly Bean upon rotation.
    904      *
    905      * Before Jelly Bean, there is a bug where setComposingRegion and other committing
    906      * functions on the input connection get ignored until the cursor moves. This method works
    907      * around the bug by wiggling the cursor first, which reactivates the connection and has
    908      * the subsequent methods work, then restoring it to its original position.
    909      *
    910      * On platforms on which this method is not present, this is a no-op.
    911      */
    912     public void maybeMoveTheCursorAroundAndRestoreToWorkaroundABug() {
    913         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
    914             if (mExpectedSelStart > 0) {
    915                 mIC.setSelection(mExpectedSelStart - 1, mExpectedSelStart - 1);
    916             } else {
    917                 mIC.setSelection(mExpectedSelStart + 1, mExpectedSelStart + 1);
    918             }
    919             mIC.setSelection(mExpectedSelStart, mExpectedSelEnd);
    920         }
    921     }
    922 
    923     private boolean mCursorAnchorInfoMonitorEnabled = false;
    924 
    925     /**
    926      * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}.
    927      * @param enableMonitor {@code true} to request the editor to call back the method whenever the
    928      * cursor/anchor position is changed.
    929      * @param requestImmediateCallback {@code true} to request the editor to call back the method
    930      * as soon as possible to notify the current cursor/anchor position to the input method.
    931      * @return {@code true} if the request is accepted. Returns {@code false} otherwise, which
    932      * includes "not implemented" or "rejected" or "temporarily unavailable" or whatever which
    933      * prevents the application from fulfilling the request. (TODO: Improve the API when it turns
    934      * out that we actually need more detailed error codes)
    935      */
    936     public boolean requestCursorUpdates(final boolean enableMonitor,
    937             final boolean requestImmediateCallback) {
    938         mIC = mParent.getCurrentInputConnection();
    939         final boolean scheduled;
    940         if (null != mIC) {
    941             scheduled = InputConnectionCompatUtils.requestCursorUpdates(mIC, enableMonitor,
    942                     requestImmediateCallback);
    943         } else {
    944             scheduled = false;
    945         }
    946         mCursorAnchorInfoMonitorEnabled = (scheduled && enableMonitor);
    947         return scheduled;
    948     }
    949 
    950     /**
    951      * @return {@code true} if the application reported that the monitor mode of
    952      * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is currently enabled.
    953      */
    954     public boolean isCursorAnchorInfoMonitorEnabled() {
    955         return mCursorAnchorInfoMonitorEnabled;
    956     }
    957 }
    958