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