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