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