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"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.inputmethodservice.InputMethodService;
     20 import android.text.TextUtils;
     21 import android.util.Log;
     22 import android.view.KeyEvent;
     23 import android.view.inputmethod.CompletionInfo;
     24 import android.view.inputmethod.CorrectionInfo;
     25 import android.view.inputmethod.ExtractedText;
     26 import android.view.inputmethod.ExtractedTextRequest;
     27 import android.view.inputmethod.InputConnection;
     28 
     29 import com.android.inputmethod.keyboard.Keyboard;
     30 import com.android.inputmethod.latin.define.ProductionFlag;
     31 import com.android.inputmethod.research.ResearchLogger;
     32 
     33 import java.util.Locale;
     34 import java.util.regex.Pattern;
     35 
     36 /**
     37  * Enrichment class for InputConnection to simplify interaction and add functionality.
     38  *
     39  * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
     40  * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
     41  * all the time to find out what text is in the buffer, when we need it to determine caps mode
     42  * for example.
     43  */
     44 public final class RichInputConnection {
     45     private static final String TAG = RichInputConnection.class.getSimpleName();
     46     private static final boolean DBG = false;
     47     private static final boolean DEBUG_PREVIOUS_TEXT = false;
     48     private static final boolean DEBUG_BATCH_NESTING = false;
     49     // Provision for a long word pair and a separator
     50     private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
     51     private static final Pattern spaceRegex = Pattern.compile("\\s+");
     52     private static final int INVALID_CURSOR_POSITION = -1;
     53 
     54     /**
     55      * This variable contains the value LatinIME thinks the cursor position should be at now.
     56      * This is a few steps in advance of what the TextView thinks it is, because TextView will
     57      * only know after the IPC calls gets through.
     58      */
     59     private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
     60     /**
     61      * This contains the committed text immediately preceding the cursor and the composing
     62      * text if any. It is refreshed when the cursor moves by calling upon the TextView.
     63      */
     64     private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
     65     /**
     66      * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
     67      */
     68     private StringBuilder mComposingText = new StringBuilder();
     69     /**
     70      * This is a one-character string containing the character after the cursor. Since LatinIME
     71      * never touches it directly, it's never modified by any means other than re-reading from the
     72      * TextView when the cursor position is changed by the user.
     73      */
     74     private CharSequence mCharAfterTheCursor = "";
     75     // A hint on how many characters to cache from the TextView. A good value of this is given by
     76     // how many characters we need to be able to almost always find the caps mode.
     77     private static final int DEFAULT_TEXT_CACHE_SIZE = 100;
     78 
     79     private final InputMethodService mParent;
     80     InputConnection mIC;
     81     int mNestLevel;
     82     public RichInputConnection(final InputMethodService parent) {
     83         mParent = parent;
     84         mIC = null;
     85         mNestLevel = 0;
     86     }
     87 
     88     private void checkConsistencyForDebug() {
     89         final ExtractedTextRequest r = new ExtractedTextRequest();
     90         r.hintMaxChars = 0;
     91         r.hintMaxLines = 0;
     92         r.token = 1;
     93         r.flags = 0;
     94         final ExtractedText et = mIC.getExtractedText(r, 0);
     95         final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
     96         final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
     97                 .append(mComposingText);
     98         if (null == et || null == beforeCursor) return;
     99         final int actualLength = Math.min(beforeCursor.length(), internal.length());
    100         if (internal.length() > actualLength) {
    101             internal.delete(0, internal.length() - actualLength);
    102         }
    103         final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
    104                 : beforeCursor.subSequence(beforeCursor.length() - actualLength,
    105                         beforeCursor.length()).toString();
    106         if (et.selectionStart != mCurrentCursorPosition
    107                 || !(reference.equals(internal.toString()))) {
    108             final String context = "Expected cursor position = " + mCurrentCursorPosition
    109                     + "\nActual cursor position = " + et.selectionStart
    110                     + "\nExpected text = " + internal.length() + " " + internal
    111                     + "\nActual text = " + reference.length() + " " + reference;
    112             ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
    113         } else {
    114             Log.e(TAG, Utils.getStackTrace(2));
    115             Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
    116         }
    117     }
    118 
    119     public void beginBatchEdit() {
    120         if (++mNestLevel == 1) {
    121             mIC = mParent.getCurrentInputConnection();
    122             if (null != mIC) {
    123                 mIC.beginBatchEdit();
    124             }
    125         } else {
    126             if (DBG) {
    127                 throw new RuntimeException("Nest level too deep");
    128             } else {
    129                 Log.e(TAG, "Nest level too deep : " + mNestLevel);
    130             }
    131         }
    132         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    133         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    134     }
    135 
    136     public void endBatchEdit() {
    137         if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
    138         if (--mNestLevel == 0 && null != mIC) {
    139             mIC.endBatchEdit();
    140         }
    141         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    142     }
    143 
    144     public void resetCachesUponCursorMove(final int newCursorPosition) {
    145         mCurrentCursorPosition = newCursorPosition;
    146         mComposingText.setLength(0);
    147         mCommittedTextBeforeComposingText.setLength(0);
    148         mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
    149         mCharAfterTheCursor = getTextAfterCursor(1, 0);
    150         if (null != mIC) {
    151             mIC.finishComposingText();
    152             if (ProductionFlag.IS_EXPERIMENTAL) {
    153                 ResearchLogger.richInputConnection_finishComposingText();
    154             }
    155         }
    156     }
    157 
    158     private void checkBatchEdit() {
    159         if (mNestLevel != 1) {
    160             // TODO: exception instead
    161             Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
    162             Log.e(TAG, Utils.getStackTrace(4));
    163         }
    164     }
    165 
    166     public void finishComposingText() {
    167         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    168         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    169         mCommittedTextBeforeComposingText.append(mComposingText);
    170         mCurrentCursorPosition += mComposingText.length();
    171         mComposingText.setLength(0);
    172         if (null != mIC) {
    173             mIC.finishComposingText();
    174             if (ProductionFlag.IS_EXPERIMENTAL) {
    175                 ResearchLogger.richInputConnection_finishComposingText();
    176             }
    177         }
    178     }
    179 
    180     public void commitText(final CharSequence text, final int i) {
    181         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    182         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    183         mCommittedTextBeforeComposingText.append(text);
    184         mCurrentCursorPosition += text.length() - mComposingText.length();
    185         mComposingText.setLength(0);
    186         if (null != mIC) {
    187             mIC.commitText(text, i);
    188             if (ProductionFlag.IS_EXPERIMENTAL) {
    189                 ResearchLogger.richInputConnection_commitText(text, i);
    190             }
    191         }
    192     }
    193 
    194     /**
    195      * Gets the caps modes we should be in after this specific string.
    196      *
    197      * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
    198      * This method also supports faking an additional space after the string passed in argument,
    199      * to support cases where a space will be added automatically, like in phantom space
    200      * state for example.
    201      * Note that for English, we are using American typography rules (which are not specific to
    202      * American English, it's just the most common set of rules for English).
    203      *
    204      * @param inputType a mask of the caps modes to test for.
    205      * @param locale what language should be considered.
    206      * @param hasSpaceBefore if we should consider there should be a space after the string.
    207      * @return the caps modes that should be on as a set of bits
    208      */
    209     public int getCursorCapsMode(final int inputType, final Locale locale,
    210             final boolean hasSpaceBefore) {
    211         mIC = mParent.getCurrentInputConnection();
    212         if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
    213         if (!TextUtils.isEmpty(mComposingText)) {
    214             if (hasSpaceBefore) {
    215                 // If we have some composing text and a space before, then we should have
    216                 // MODE_CHARACTERS and MODE_WORDS on.
    217                 return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
    218             } else {
    219                 // We have some composing text - we should be in MODE_CHARACTERS only.
    220                 return TextUtils.CAP_MODE_CHARACTERS & inputType;
    221             }
    222         }
    223         // TODO: this will generally work, but there may be cases where the buffer contains SOME
    224         // information but not enough to determine the caps mode accurately. This may happen after
    225         // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so.
    226         // getCapsMode should be updated to be able to return a "not enough info" result so that
    227         // we can get more context only when needed.
    228         if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mCurrentCursorPosition) {
    229             mCommittedTextBeforeComposingText.append(
    230                     getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
    231         }
    232         // This never calls InputConnection#getCapsMode - in fact, it's a static method that
    233         // never blocks or initiates IPC.
    234         return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale,
    235                 hasSpaceBefore);
    236     }
    237 
    238     public int getCodePointBeforeCursor() {
    239         if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE;
    240         return Character.codePointBefore(mCommittedTextBeforeComposingText,
    241                 mCommittedTextBeforeComposingText.length());
    242     }
    243 
    244     public CharSequence getTextBeforeCursor(final int i, final int j) {
    245         // TODO: use mCommittedTextBeforeComposingText if possible to improve performance
    246         mIC = mParent.getCurrentInputConnection();
    247         if (null != mIC) return mIC.getTextBeforeCursor(i, j);
    248         return null;
    249     }
    250 
    251     public CharSequence getTextAfterCursor(final int i, final int j) {
    252         mIC = mParent.getCurrentInputConnection();
    253         if (null != mIC) return mIC.getTextAfterCursor(i, j);
    254         return null;
    255     }
    256 
    257     public void deleteSurroundingText(final int i, final int j) {
    258         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    259         final int remainingChars = mComposingText.length() - i;
    260         if (remainingChars >= 0) {
    261             mComposingText.setLength(remainingChars);
    262         } else {
    263             mComposingText.setLength(0);
    264             // Never cut under 0
    265             final int len = Math.max(mCommittedTextBeforeComposingText.length()
    266                     + remainingChars, 0);
    267             mCommittedTextBeforeComposingText.setLength(len);
    268         }
    269         if (mCurrentCursorPosition > i) {
    270             mCurrentCursorPosition -= i;
    271         } else {
    272             mCurrentCursorPosition = 0;
    273         }
    274         if (null != mIC) {
    275             mIC.deleteSurroundingText(i, j);
    276             if (ProductionFlag.IS_EXPERIMENTAL) {
    277                 ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
    278             }
    279         }
    280         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    281     }
    282 
    283     public void performEditorAction(final int actionId) {
    284         mIC = mParent.getCurrentInputConnection();
    285         if (null != mIC) {
    286             mIC.performEditorAction(actionId);
    287             if (ProductionFlag.IS_EXPERIMENTAL) {
    288                 ResearchLogger.richInputConnection_performEditorAction(actionId);
    289             }
    290         }
    291     }
    292 
    293     public void sendKeyEvent(final KeyEvent keyEvent) {
    294         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    295         if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
    296             if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    297             // This method is only called for enter or backspace when speaking to old
    298             // applications (target SDK <= 15), or for digits.
    299             // When talking to new applications we never use this method because it's inherently
    300             // racy and has unpredictable results, but for backward compatibility we continue
    301             // sending the key events for only Enter and Backspace because some applications
    302             // mistakenly catch them to do some stuff.
    303             switch (keyEvent.getKeyCode()) {
    304                 case KeyEvent.KEYCODE_ENTER:
    305                     mCommittedTextBeforeComposingText.append("\n");
    306                     mCurrentCursorPosition += 1;
    307                     break;
    308                 case KeyEvent.KEYCODE_DEL:
    309                     if (0 == mComposingText.length()) {
    310                         if (mCommittedTextBeforeComposingText.length() > 0) {
    311                             mCommittedTextBeforeComposingText.delete(
    312                                     mCommittedTextBeforeComposingText.length() - 1,
    313                                     mCommittedTextBeforeComposingText.length());
    314                         }
    315                     } else {
    316                         mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
    317                     }
    318                     if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
    319                     break;
    320                 case KeyEvent.KEYCODE_UNKNOWN:
    321                     if (null != keyEvent.getCharacters()) {
    322                         mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
    323                         mCurrentCursorPosition += keyEvent.getCharacters().length();
    324                     }
    325                     break;
    326                 default:
    327                     final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
    328                     mCommittedTextBeforeComposingText.append(text);
    329                     mCurrentCursorPosition += text.length();
    330                     break;
    331             }
    332         }
    333         if (null != mIC) {
    334             mIC.sendKeyEvent(keyEvent);
    335             if (ProductionFlag.IS_EXPERIMENTAL) {
    336                 ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
    337             }
    338         }
    339     }
    340 
    341     public void setComposingText(final CharSequence text, final int i) {
    342         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    343         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    344         mCurrentCursorPosition += text.length() - mComposingText.length();
    345         mComposingText.setLength(0);
    346         mComposingText.append(text);
    347         // TODO: support values of i != 1. At this time, this is never called with i != 1.
    348         if (null != mIC) {
    349             mIC.setComposingText(text, i);
    350             if (ProductionFlag.IS_EXPERIMENTAL) {
    351                 ResearchLogger.richInputConnection_setComposingText(text, i);
    352             }
    353         }
    354         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    355     }
    356 
    357     public void setSelection(final int from, final int to) {
    358         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    359         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    360         if (null != mIC) {
    361             mIC.setSelection(from, to);
    362             if (ProductionFlag.IS_EXPERIMENTAL) {
    363                 ResearchLogger.richInputConnection_setSelection(from, to);
    364             }
    365         }
    366         mCurrentCursorPosition = from;
    367         mCommittedTextBeforeComposingText.setLength(0);
    368         mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
    369     }
    370 
    371     public void commitCorrection(final CorrectionInfo correctionInfo) {
    372         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    373         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    374         // This has no effect on the text field and does not change its content. It only makes
    375         // TextView flash the text for a second based on indices contained in the argument.
    376         if (null != mIC) {
    377             mIC.commitCorrection(correctionInfo);
    378             if (ProductionFlag.IS_EXPERIMENTAL) {
    379                 ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
    380             }
    381         }
    382         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    383     }
    384 
    385     public void commitCompletion(final CompletionInfo completionInfo) {
    386         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    387         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    388         final CharSequence text = completionInfo.getText();
    389         mCommittedTextBeforeComposingText.append(text);
    390         mCurrentCursorPosition += text.length() - mComposingText.length();
    391         mComposingText.setLength(0);
    392         if (null != mIC) {
    393             mIC.commitCompletion(completionInfo);
    394             if (ProductionFlag.IS_EXPERIMENTAL) {
    395                 ResearchLogger.richInputConnection_commitCompletion(completionInfo);
    396             }
    397         }
    398         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
    399     }
    400 
    401     public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
    402         mIC = mParent.getCurrentInputConnection();
    403         if (null == mIC) return null;
    404         final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
    405         if (DEBUG_PREVIOUS_TEXT && null != prev) {
    406             final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
    407             final String reference = prev.length() <= checkLength ? prev.toString()
    408                     : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
    409             final StringBuilder internal = new StringBuilder()
    410                     .append(mCommittedTextBeforeComposingText).append(mComposingText);
    411             if (internal.length() > checkLength) {
    412                 internal.delete(0, internal.length() - checkLength);
    413                 if (!(reference.equals(internal.toString()))) {
    414                     final String context =
    415                             "Expected text = " + internal + "\nActual text = " + reference;
    416                     ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
    417                 }
    418             }
    419         }
    420         return getNthPreviousWord(prev, sentenceSeperators, n);
    421     }
    422 
    423     /**
    424      * Represents a range of text, relative to the current cursor position.
    425      */
    426     public static final class Range {
    427         /** Characters before selection start */
    428         public final int mCharsBefore;
    429 
    430         /**
    431          * Characters after selection start, including one trailing word
    432          * separator.
    433          */
    434         public final int mCharsAfter;
    435 
    436         /** The actual characters that make up a word */
    437         public final String mWord;
    438 
    439         public Range(int charsBefore, int charsAfter, String word) {
    440             if (charsBefore < 0 || charsAfter < 0) {
    441                 throw new IndexOutOfBoundsException();
    442             }
    443             this.mCharsBefore = charsBefore;
    444             this.mCharsAfter = charsAfter;
    445             this.mWord = word;
    446         }
    447     }
    448 
    449     private static boolean isSeparator(int code, String sep) {
    450         return sep.indexOf(code) != -1;
    451     }
    452 
    453     // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
    454     // n = 2 retrieves the word before that, and so on. This splits on whitespace only.
    455     // Also, it won't return words that end in a separator (if the nth word before the cursor
    456     // ends in a separator, it returns null).
    457     // Example :
    458     // (n = 1) "abc def|" -> def
    459     // (n = 1) "abc def |" -> def
    460     // (n = 1) "abc def. |" -> null
    461     // (n = 1) "abc def . |" -> null
    462     // (n = 2) "abc def|" -> abc
    463     // (n = 2) "abc def |" -> abc
    464     // (n = 2) "abc def. |" -> abc
    465     // (n = 2) "abc def . |" -> def
    466     // (n = 2) "abc|" -> null
    467     // (n = 2) "abc |" -> null
    468     // (n = 2) "abc. def|" -> null
    469     public static CharSequence getNthPreviousWord(final CharSequence prev,
    470             final String sentenceSeperators, final int n) {
    471         if (prev == null) return null;
    472         String[] w = spaceRegex.split(prev);
    473 
    474         // If we can't find n words, or we found an empty word, return null.
    475         if (w.length < n || w[w.length - n].length() <= 0) return null;
    476 
    477         // If ends in a separator, return null
    478         char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1);
    479         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
    480 
    481         return w[w.length - n];
    482     }
    483 
    484     /**
    485      * @param separators characters which may separate words
    486      * @return the word that surrounds the cursor, including up to one trailing
    487      *   separator. For example, if the field contains "he|llo world", where |
    488      *   represents the cursor, then "hello " will be returned.
    489      */
    490     public String getWordAtCursor(String separators) {
    491         // getWordRangeAtCursor returns null if the connection is null
    492         Range r = getWordRangeAtCursor(separators, 0);
    493         return (r == null) ? null : r.mWord;
    494     }
    495 
    496     private int getCursorPosition() {
    497         mIC = mParent.getCurrentInputConnection();
    498         if (null == mIC) return INVALID_CURSOR_POSITION;
    499         final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
    500         if (extracted == null) {
    501             return INVALID_CURSOR_POSITION;
    502         }
    503         return extracted.startOffset + extracted.selectionStart;
    504     }
    505 
    506     /**
    507      * Returns the text surrounding the cursor.
    508      *
    509      * @param sep a string of characters that split words.
    510      * @param additionalPrecedingWordsCount the number of words before the current word that should
    511      *   be included in the returned range
    512      * @return a range containing the text surrounding the cursor
    513      */
    514     public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
    515         mIC = mParent.getCurrentInputConnection();
    516         if (mIC == null || sep == null) {
    517             return null;
    518         }
    519         CharSequence before = mIC.getTextBeforeCursor(1000, 0);
    520         CharSequence after = mIC.getTextAfterCursor(1000, 0);
    521         if (before == null || after == null) {
    522             return null;
    523         }
    524 
    525         // Going backward, alternate skipping non-separators and separators until enough words
    526         // have been read.
    527         int start = before.length();
    528         boolean isStoppingAtWhitespace = true;  // toggles to indicate what to stop at
    529         while (true) { // see comments below for why this is guaranteed to halt
    530             while (start > 0) {
    531                 final int codePoint = Character.codePointBefore(before, start);
    532                 if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
    533                     break;  // inner loop
    534                 }
    535                 --start;
    536                 if (Character.isSupplementaryCodePoint(codePoint)) {
    537                     --start;
    538                 }
    539             }
    540             // isStoppingAtWhitespace is true every other time through the loop,
    541             // so additionalPrecedingWordsCount is guaranteed to become < 0, which
    542             // guarantees outer loop termination
    543             if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
    544                 break;  // outer loop
    545             }
    546             isStoppingAtWhitespace = !isStoppingAtWhitespace;
    547         }
    548 
    549         // Find last word separator after the cursor
    550         int end = -1;
    551         while (++end < after.length()) {
    552             final int codePoint = Character.codePointAt(after, end);
    553             if (isSeparator(codePoint, sep)) {
    554                 break;
    555             }
    556             if (Character.isSupplementaryCodePoint(codePoint)) {
    557                 ++end;
    558             }
    559         }
    560 
    561         int cursor = getCursorPosition();
    562         if (start >= 0 && cursor + end <= after.length() + before.length()) {
    563             String word = before.toString().substring(start, before.length())
    564                     + after.toString().substring(0, end);
    565             return new Range(before.length() - start, end, word);
    566         }
    567 
    568         return null;
    569     }
    570 
    571     public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
    572         CharSequence before = getTextBeforeCursor(1, 0);
    573         CharSequence after = getTextAfterCursor(1, 0);
    574         if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
    575                 && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
    576             return true;
    577         }
    578         if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
    579                 && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
    580             return true;
    581         }
    582         return false;
    583     }
    584 
    585     public void removeTrailingSpace() {
    586         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    587         final CharSequence lastOne = getTextBeforeCursor(1, 0);
    588         if (lastOne != null && lastOne.length() == 1
    589                 && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
    590             deleteSurroundingText(1, 0);
    591         }
    592     }
    593 
    594     public boolean sameAsTextBeforeCursor(final CharSequence text) {
    595         final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
    596         return TextUtils.equals(text, beforeText);
    597     }
    598 
    599     /* (non-javadoc)
    600      * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
    601      */
    602     public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
    603         // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
    604         // separator or end of line/text)
    605         // Example: "test|"<EOL> "te|st" get rejected here
    606         final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
    607         if (!TextUtils.isEmpty(textAfterCursor)
    608                 && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
    609 
    610         // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
    611         // Example: " -|" gets rejected here but "e-|" and "e|" are okay
    612         CharSequence word = getWordAtCursor(settings.mWordSeparators);
    613         // We don't suggest on leading single quotes, so we have to remove them from the word if
    614         // it starts with single quotes.
    615         while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
    616             word = word.subSequence(1, word.length());
    617         }
    618         if (TextUtils.isEmpty(word)) return null;
    619         // Find the last code point of the string
    620         final int lastCodePoint = Character.codePointBefore(word, word.length());
    621         // If for some reason the text field contains non-unicode binary data, or if the
    622         // charsequence is exactly one char long and the contents is a low surrogate, return null.
    623         if (!Character.isDefined(lastCodePoint)) return null;
    624         // Bail out if the cursor is not at the end of a word (cursor must be preceded by
    625         // non-whitespace, non-separator, non-start-of-text)
    626         // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
    627         if (settings.isWordSeparator(lastCodePoint)) return null;
    628         final char firstChar = word.charAt(0); // we just tested that word is not empty
    629         if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
    630 
    631         // We only suggest on words that start with a letter or a symbol that is excluded from
    632         // word separators (see #handleCharacterWhileInBatchEdit).
    633         if (!(Character.isLetter(firstChar)
    634                 || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
    635             return null;
    636         }
    637 
    638         return word;
    639     }
    640 
    641     public boolean revertDoubleSpace() {
    642         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    643         // Here we test whether we indeed have a period and a space before us. This should not
    644         // be needed, but it's there just in case something went wrong.
    645         final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
    646         if (!". ".equals(textBeforeCursor)) {
    647             // Theoretically we should not be coming here if there isn't ". " before the
    648             // cursor, but the application may be changing the text while we are typing, so
    649             // anything goes. We should not crash.
    650             Log.d(TAG, "Tried to revert double-space combo but we didn't find "
    651                     + "\". \" just before the cursor.");
    652             return false;
    653         }
    654         deleteSurroundingText(2, 0);
    655         commitText("  ", 1);
    656         return true;
    657     }
    658 
    659     public boolean revertSwapPunctuation() {
    660         if (DEBUG_BATCH_NESTING) checkBatchEdit();
    661         // Here we test whether we indeed have a space and something else before us. This should not
    662         // be needed, but it's there just in case something went wrong.
    663         final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
    664         // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
    665         // enter surrogate pairs this code will have been removed.
    666         if (TextUtils.isEmpty(textBeforeCursor)
    667                 || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
    668             // We may only come here if the application is changing the text while we are typing.
    669             // This is quite a broken case, but not logically impossible, so we shouldn't crash,
    670             // but some debugging log may be in order.
    671             Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
    672                     + "find a space just before the cursor.");
    673             return false;
    674         }
    675         deleteSurroundingText(2, 0);
    676         commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
    677         return true;
    678     }
    679 
    680     /**
    681      * Heuristic to determine if this is an expected update of the cursor.
    682      *
    683      * Sometimes updates to the cursor position are late because of their asynchronous nature.
    684      * This method tries to determine if this update is one, based on the values of the cursor
    685      * position in the update, and the currently expected position of the cursor according to
    686      * LatinIME's internal accounting. If this is not a belated expected update, then it should
    687      * mean that the user moved the cursor explicitly.
    688      * This is quite robust, but of course it's not perfect. In particular, it will fail in the
    689      * case we get an update A, the user types in N characters so as to move the cursor to A+N but
    690      * we don't get those, and then the user places the cursor between A and A+N, and we get only
    691      * this update and not the ones in-between. This is almost impossible to achieve even trying
    692      * very very hard.
    693      *
    694      * @param oldSelStart The value of the old cursor position in the update.
    695      * @param newSelStart The value of the new cursor position in the update.
    696      * @return whether this is a belated expected update or not.
    697      */
    698     public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) {
    699         // If this is an update that arrives at our expected position, it's a belated update.
    700         if (newSelStart == mCurrentCursorPosition) return true;
    701         // If this is an update that moves the cursor from our expected position, it must be
    702         // an explicit move.
    703         if (oldSelStart == mCurrentCursorPosition) return false;
    704         // The following returns true if newSelStart is between oldSelStart and
    705         // mCurrentCursorPosition. We assume that if the updated position is between the old
    706         // position and the expected position, then it must be a belated update.
    707         return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0;
    708     }
    709 }
    710