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