1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.omnibox;
6 
7 import android.os.Bundle;
8 import android.text.Editable;
9 import android.text.Selection;
10 import android.text.SpannableString;
11 import android.text.Spanned;
12 import android.text.style.BackgroundColorSpan;
13 import android.view.KeyEvent;
14 import android.view.accessibility.AccessibilityEvent;
15 import android.view.inputmethod.BaseInputConnection;
16 import android.view.inputmethod.CompletionInfo;
17 import android.view.inputmethod.CorrectionInfo;
18 import android.view.inputmethod.ExtractedText;
19 import android.view.inputmethod.ExtractedTextRequest;
20 import android.view.inputmethod.InputConnection;
21 import android.view.inputmethod.InputConnectionWrapper;
22 import android.view.inputmethod.InputContentInfo;
23 
24 import androidx.annotation.VisibleForTesting;
25 
26 import org.chromium.base.Log;
27 
28 import java.util.Locale;
29 import java.util.regex.Pattern;
30 
31 /**
32  * An autocomplete model that appends autocomplete text at the end of query/URL text as
33  * SpannableString. By wrapping all the keyboard related operations in a batch edit, we can
34  * effectively hide the existence of autocomplete text from keyboard.
35  */
36 public class SpannableAutocompleteEditTextModel implements AutocompleteEditTextModelBase {
37     private static final String TAG = "SpanAutocomplete";
38 
39     private static final boolean DEBUG = false;
40 
41     // A pattern that matches strings consisting of English and European character sets, numbers,
42     // punctuations, and a white space.
43     private static final Pattern NON_COMPOSITIONAL_TEXT_PATTERN = Pattern.compile(
44             "[\\p{script=latin}\\p{script=cyrillic}\\p{script=greek}\\p{script=hebrew}\\p{Punct} "
45             + "0-9]*");
46 
47     private final AutocompleteEditTextModelBase.Delegate mDelegate;
48 
49     // The current state that reflects EditText view's current state through callbacks such as
50     // onSelectionChanged() and onTextChanged(). It reflects all the latest changes even in a batch
51     // edit. The autocomplete text here is meaningless in the middle of a change.
52     private final AutocompleteState mCurrentState;
53 
54     // This keeps track of the state in which previous notification was sent. It prevents redundant
55     // or unnecessary notification.
56     private final AutocompleteState mPreviouslyNotifiedState;
57 
58     // This keeps track of the autocompletetext that we need to show (at the end of batch edit if
59     // we are in the middle of it), and also the matching user text that it should be appended to.
60     // Note that this potentially allows the controller to update in a delayed manner.
61     private final AutocompleteState mPreviouslySetState;
62 
63     private final SpanCursorController mSpanCursorController;
64 
65     private AutocompleteInputConnection mInputConnection;
66     private boolean mLastEditWasTyping = true;
67     private boolean mIgnoreTextChangeFromAutocomplete = true;
68     private int mBatchEditNestCount;
69     private int mDeletePostfixOnNextBeginImeCommand;
70 
71     // For testing.
72     private int mLastUpdateSelStart;
73     private int mLastUpdateSelEnd;
74 
75     // This controls whether AutocompleteEditText is permitted to pass-through specific
76     // Accessibility announcements, in particular the TEXT_CHANGED and TEXT_SELECTION_CHANGED.
77     // The only events of the above type that are allowed are ones coming from
78     // SpannableAutocompleteEditTextModel.
79     private boolean mDelegateShouldIgnoreAccessibilityEvents = true;
80 
SpannableAutocompleteEditTextModel(AutocompleteEditTextModelBase.Delegate delegate)81     public SpannableAutocompleteEditTextModel(AutocompleteEditTextModelBase.Delegate delegate) {
82         if (DEBUG) Log.i(TAG, "constructor");
83         mDelegate = delegate;
84         mCurrentState = new AutocompleteState(delegate.getText().toString(), "",
85                 delegate.getSelectionStart(), delegate.getSelectionEnd());
86         mPreviouslyNotifiedState = new AutocompleteState(mCurrentState);
87         mPreviouslySetState = new AutocompleteState(mCurrentState);
88 
89         mSpanCursorController = new SpanCursorController(delegate);
90     }
91 
92     @Override
onCreateInputConnection(InputConnection inputConnection)93     public InputConnection onCreateInputConnection(InputConnection inputConnection) {
94         mLastUpdateSelStart = mDelegate.getSelectionStart();
95         mLastUpdateSelEnd = mDelegate.getSelectionEnd();
96         mBatchEditNestCount = 0;
97         if (inputConnection == null) {
98             if (DEBUG) Log.i(TAG, "onCreateInputConnection: null");
99             mInputConnection = null;
100             return null;
101         }
102         if (DEBUG) Log.i(TAG, "onCreateInputConnection");
103         mInputConnection = new AutocompleteInputConnection();
104         mInputConnection.setTarget(inputConnection);
105         return mInputConnection;
106     }
107 
108     /**
109      * @param editable The editable.
110      * @return Debug string for the given {@Editable}.
111      */
getEditableDebugString(Editable editable)112     private static String getEditableDebugString(Editable editable) {
113         return String.format(Locale.US, "Editable {[%s] SEL[%d %d] COM[%d %d]}",
114                 editable.toString(), Selection.getSelectionStart(editable),
115                 Selection.getSelectionEnd(editable),
116                 BaseInputConnection.getComposingSpanStart(editable),
117                 BaseInputConnection.getComposingSpanEnd(editable));
118     }
119 
sendAccessibilityEventForUserTextChange( AutocompleteState oldState, AutocompleteState newState)120     private void sendAccessibilityEventForUserTextChange(
121             AutocompleteState oldState, AutocompleteState newState) {
122         int addedCount = -1;
123         int removedCount = -1;
124         int fromIndex = -1;
125 
126         if (newState.isBackwardDeletedFrom(oldState)) {
127             addedCount = 0;
128             removedCount = oldState.getText().length() - newState.getUserText().length();
129             fromIndex = newState.getUserText().length();
130         } else if (newState.isForwardTypedFrom(oldState)) {
131             addedCount = newState.getUserText().length() - oldState.getUserText().length();
132             removedCount = oldState.getAutocompleteText().length();
133             fromIndex = oldState.getUserText().length();
134         } else if (newState.getUserText().equals(oldState.getUserText())) {
135             addedCount = 0;
136             removedCount = oldState.getAutocompleteText().length();
137             fromIndex = oldState.getUserText().length();
138         } else {
139             // Assume that the whole text has been replaced.
140             addedCount = newState.getText().length();
141             removedCount = oldState.getUserText().length();
142             fromIndex = 0;
143         }
144 
145         mDelegateShouldIgnoreAccessibilityEvents = false;
146         if (!oldState.getText().equals(newState.getText())
147                 && (addedCount != 0 || removedCount != 0)) {
148             AccessibilityEvent event =
149                     AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
150             event.setBeforeText(oldState.getText());
151             event.setFromIndex(fromIndex);
152             event.setRemovedCount(removedCount);
153             event.setAddedCount(addedCount);
154             mDelegate.sendAccessibilityEventUnchecked(event);
155         }
156 
157         if (oldState.getSelStart() != newState.getSelStart()
158                 || oldState.getSelEnd() != newState.getSelEnd()) {
159             mDelegate.sendAccessibilityEventUnchecked(
160                     AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED));
161         }
162         mDelegateShouldIgnoreAccessibilityEvents = true;
163     }
164 
sendAccessibilityEventForAppendingAutocomplete(AutocompleteState newState)165     private void sendAccessibilityEventForAppendingAutocomplete(AutocompleteState newState) {
166         if (!newState.hasAutocompleteText()) return;
167         // Note that only text changes and selection does not change.
168         AccessibilityEvent eventTextChanged =
169                 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
170         eventTextChanged.setBeforeText(newState.getUserText());
171         eventTextChanged.setFromIndex(newState.getUserText().length());
172         eventTextChanged.setRemovedCount(0);
173         eventTextChanged.setAddedCount(newState.getAutocompleteText().length());
174         mDelegateShouldIgnoreAccessibilityEvents = false;
175         mDelegate.sendAccessibilityEventUnchecked(eventTextChanged);
176         mDelegateShouldIgnoreAccessibilityEvents = true;
177     }
178 
notifyAccessibilityService()179     private void notifyAccessibilityService() {
180         if (mCurrentState.equals(mPreviouslyNotifiedState)) return;
181         if (!mDelegate.isAccessibilityEnabled()) return;
182         sendAccessibilityEventForUserTextChange(mPreviouslyNotifiedState, mCurrentState);
183         // Read autocomplete text separately.
184         sendAccessibilityEventForAppendingAutocomplete(mCurrentState);
185     }
186 
notifyAutocompleteTextStateChanged()187     private void notifyAutocompleteTextStateChanged() {
188         if (DEBUG) {
189             Log.i(TAG, "notifyAutocompleteTextStateChanged PRV[%s] CUR[%s] IGN[%b]",
190                     mPreviouslyNotifiedState, mCurrentState, mIgnoreTextChangeFromAutocomplete);
191         }
192         if (mBatchEditNestCount > 0) {
193             // crbug.com/764749
194             Log.w(TAG, "Did not notify - in batch edit.");
195             return;
196         }
197         if (mCurrentState.equals(mPreviouslyNotifiedState)) {
198             // crbug.com/764749
199             Log.w(TAG, "Did not notify - no change.");
200             return;
201         }
202         notifyAccessibilityService();
203         if (mCurrentState.getUserText().equals(mPreviouslyNotifiedState.getUserText())
204                 && (mCurrentState.hasAutocompleteText()
205                            || !mPreviouslyNotifiedState.hasAutocompleteText())) {
206             // Nothing has changed except that autocomplete text has been set or modified. Or
207             // selection change did not affect autocomplete text. Autocomplete text is set by the
208             // controller, so only text change or deletion of autocomplete text should be notified.
209             mPreviouslyNotifiedState.copyFrom(mCurrentState);
210             return;
211         }
212         mPreviouslyNotifiedState.copyFrom(mCurrentState);
213         if (mIgnoreTextChangeFromAutocomplete) {
214             // crbug.com/764749
215             Log.w(TAG, "Did not notify - ignored.");
216             return;
217         }
218         // The current model's mechanism always moves the cursor at the end of user text, so we
219         // don't need to update the display.
220         mDelegate.onAutocompleteTextStateChanged(false /* updateDisplay */);
221     }
222 
clearAutocompleteText()223     private void clearAutocompleteText() {
224         if (DEBUG) Log.i(TAG, "clearAutocomplete");
225         mPreviouslySetState.clearAutocompleteText();
226         mCurrentState.clearAutocompleteText();
227     }
228 
clearAutocompleteTextAndUpdateSpanCursor()229     private void clearAutocompleteTextAndUpdateSpanCursor() {
230         if (DEBUG) Log.i(TAG, "clearAutocompleteAndUpdateSpanCursor");
231         clearAutocompleteText();
232         // Take effect and notify if not already in a batch edit.
233         if (mInputConnection != null) {
234             mInputConnection.onBeginImeCommand();
235             mInputConnection.onEndImeCommand();
236         } else {
237             mSpanCursorController.removeSpan();
238             notifyAutocompleteTextStateChanged();
239         }
240     }
241 
242     @Override
dispatchKeyEvent(final KeyEvent event)243     public boolean dispatchKeyEvent(final KeyEvent event) {
244         if (DEBUG) Log.i(TAG, "dispatchKeyEvent");
245         if (mInputConnection == null) {
246             return mDelegate.super_dispatchKeyEvent(event);
247         }
248         mInputConnection.onBeginImeCommand();
249         if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
250                 && event.getAction() == KeyEvent.ACTION_DOWN) {
251             mInputConnection.commitAutocomplete();
252         }
253         boolean retVal = mDelegate.super_dispatchKeyEvent(event);
254         mInputConnection.onEndImeCommand();
255         return retVal;
256     }
257 
258     @Override
onSetText(CharSequence text)259     public void onSetText(CharSequence text) {
260         if (DEBUG) Log.i(TAG, "onSetText: " + text);
261         // setText() does not necessarily trigger onTextChanged(). We need to accept the new text
262         // and reset the states.
263         mCurrentState.set(text.toString(), "", text.length(), text.length());
264         mSpanCursorController.reset();
265         mPreviouslyNotifiedState.copyFrom(mCurrentState);
266         mPreviouslySetState.copyFrom(mCurrentState);
267         if (mBatchEditNestCount == 0) updateSelectionForTesting();
268     }
269 
270     @Override
onSelectionChanged(int selStart, int selEnd)271     public void onSelectionChanged(int selStart, int selEnd) {
272         if (DEBUG) Log.i(TAG, "onSelectionChanged [%d,%d]", selStart, selEnd);
273         if (mCurrentState.getSelStart() == selStart && mCurrentState.getSelEnd() == selEnd) return;
274 
275         mCurrentState.setSelection(selStart, selEnd);
276         if (mBatchEditNestCount > 0) return;
277         int len = mCurrentState.getUserText().length();
278         if (mCurrentState.hasAutocompleteText()) {
279             if (selStart > len || selEnd > len) {
280                 if (DEBUG) Log.i(TAG, "Autocomplete text is being touched. Make it real.");
281                 if (mInputConnection != null) mInputConnection.commitAutocomplete();
282             } else {
283                 if (DEBUG) Log.i(TAG, "Touching before the cursor removes autocomplete.");
284                 clearAutocompleteTextAndUpdateSpanCursor();
285             }
286         }
287         updateSelectionForTesting();
288         notifyAutocompleteTextStateChanged();
289     }
290 
291     @Override
onFocusChanged(boolean focused)292     public void onFocusChanged(boolean focused) {
293         if (DEBUG) Log.i(TAG, "onFocusChanged: " + focused);
294 
295         if (!focused) {
296             // Reset selection now. It will be updated immediately after focus is re-gained.
297             // We do this to ensure the selection changed announcements are advertised by us
298             // since we suppress all TEXT_SELECTION_CHANGED announcements coming from EditText.
299             mPreviouslyNotifiedState.setSelection(-1, -1);
300             mCurrentState.setSelection(-1, -1);
301         }
302     }
303 
304     @Override
onTextChanged(CharSequence text, int start, int beforeLength, int afterLength)305     public void onTextChanged(CharSequence text, int start, int beforeLength, int afterLength) {
306         if (DEBUG) Log.i(TAG, "onTextChanged: " + text);
307         mSpanCursorController.reflectTextUpdateInState(mCurrentState, text);
308         if (mBatchEditNestCount > 0) return; // let endBatchEdit() handles changes from IME.
309         // An external change such as text paste occurred.
310         mLastEditWasTyping = false;
311         clearAutocompleteTextAndUpdateSpanCursor();
312     }
313 
314     @Override
onPaste()315     public void onPaste() {
316         if (DEBUG) Log.i(TAG, "onPaste");
317     }
318 
319     @Override
getTextWithAutocomplete()320     public String getTextWithAutocomplete() {
321         String retVal = mCurrentState.getText();
322         if (DEBUG) Log.i(TAG, "getTextWithAutocomplete: %s", retVal);
323         return retVal;
324     }
325 
326     @Override
getTextWithoutAutocomplete()327     public String getTextWithoutAutocomplete() {
328         String retVal = mCurrentState.getUserText();
329         if (DEBUG) Log.i(TAG, "getTextWithoutAutocomplete: " + retVal);
330         return retVal;
331     }
332 
333     @Override
getAutocompleteText()334     public String getAutocompleteText() {
335         return mCurrentState.getAutocompleteText();
336     }
337 
338     @Override
setIgnoreTextChangeFromAutocomplete(boolean ignore)339     public void setIgnoreTextChangeFromAutocomplete(boolean ignore) {
340         if (DEBUG) Log.i(TAG, "setIgnoreText: " + ignore);
341         mIgnoreTextChangeFromAutocomplete = ignore;
342     }
343 
344     @Override
setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText)345     public void setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText) {
346         setAutocompleteTextInternal(userText.toString(), inlineAutocompleteText.toString());
347     }
348 
setAutocompleteTextInternal(String userText, String autocompleteText)349     private void setAutocompleteTextInternal(String userText, String autocompleteText) {
350         if (DEBUG) Log.i(TAG, "setAutocompleteText: %s[%s]", userText, autocompleteText);
351         mPreviouslySetState.set(userText, autocompleteText, userText.length(), userText.length());
352         // TODO(changwan): avoid any unnecessary removal and addition of autocomplete text when it
353         // is not changed or when it is appended to the existing autocomplete text.
354         if (mInputConnection != null) {
355             mInputConnection.onBeginImeCommand();
356             mInputConnection.onEndImeCommand();
357         }
358     }
359 
360     @Override
shouldAutocomplete()361     public boolean shouldAutocomplete() {
362         boolean retVal = mBatchEditNestCount == 0 && mLastEditWasTyping
363                 && mCurrentState.isCursorAtEndOfUserText() && !isKeyboardBlacklisted()
364                 && isNonCompositionalText(getTextWithoutAutocomplete());
365         if (DEBUG) Log.i(TAG, "shouldAutocomplete: " + retVal);
366         return retVal;
367     }
368 
isKeyboardBlacklisted()369     private boolean isKeyboardBlacklisted() {
370         String pkgName = mDelegate.getKeyboardPackageName();
371         return pkgName.contains(".iqqi") // crbug.com/767016
372                 || pkgName.contains("omronsoft") || pkgName.contains(".iwnn"); // crbug.com/758443
373     }
374 
shouldFinishCompositionOnDeletion()375     private boolean shouldFinishCompositionOnDeletion() {
376         // crbug.com/758443, crbug.com/766888: Japanese keyboard does not finish composition when we
377         // restore the deleted text, and later typing will make Japanese keyboard move before the
378         // restored character. Most keyboards accept finishComposingText and update their internal
379         // states.
380         String pkgName = mDelegate.getKeyboardPackageName();
381         // One exception is the recent version of Samsung keyboard which works goofily only
382         // when we finish composing text here. Since it is more difficult to blacklist all Japanese
383         // keyboards, instead we call finishComposingText() for all the keyboards except for Samsung
384         // keyboard.
385         return !pkgName.contains("com.sec.android.inputmethod")
386                 // crbug.com/1071011: LG keyboard has the same issue.
387                 && !pkgName.contains("com.lge.ime");
388     }
389 
390     @VisibleForTesting
isNonCompositionalText(String text)391     public static boolean isNonCompositionalText(String text) {
392         // To start with, we are only activating this for English alphabets, European characters,
393         // numbers and URLs to avoid potential bad interactions with more complex IMEs.
394         // The rationale for including character sets with diacritical marks is that backspacing on
395         // a letter with a diacritical mark most likely deletes the whole character instead of
396         // removing the diacritical mark.
397         // TODO(changwan): also scan for other traditionally non-IME charsets.
398         return NON_COMPOSITIONAL_TEXT_PATTERN.matcher(text).matches();
399     }
400 
401     @Override
hasAutocomplete()402     public boolean hasAutocomplete() {
403         boolean retVal = mCurrentState.hasAutocompleteText();
404         if (DEBUG) Log.i(TAG, "hasAutocomplete: " + retVal);
405         return retVal;
406     }
407 
408     @Override
getInputConnection()409     public InputConnection getInputConnection() {
410         return mInputConnection;
411     }
412 
updateSelectionForTesting()413     private void updateSelectionForTesting() {
414         int selStart = mDelegate.getSelectionStart();
415         int selEnd = mDelegate.getSelectionEnd();
416         if (selStart == mLastUpdateSelStart && selEnd == mLastUpdateSelEnd) return;
417 
418         mLastUpdateSelStart = selStart;
419         mLastUpdateSelEnd = selEnd;
420         mDelegate.onUpdateSelectionForTesting(selStart, selEnd);
421     }
422 
423     @Override
shouldIgnoreAccessibilityEvent()424     public boolean shouldIgnoreAccessibilityEvent() {
425         return mDelegateShouldIgnoreAccessibilityEvents;
426     }
427 
428     /**
429      * A class to set and remove, or do other operations on Span and SpannableString of autocomplete
430      * text that will be appended to the user text. In addition, cursor will be hidden whenever we
431      * are showing span to the user.
432      */
433     private static class SpanCursorController {
434         private final Delegate mDelegate;
435         private BackgroundColorSpan mSpan;
436 
SpanCursorController(Delegate delegate)437         public SpanCursorController(Delegate delegate) {
438             mDelegate = delegate;
439         }
440 
setSpan(AutocompleteState state)441         public void setSpan(AutocompleteState state) {
442             int sel = state.getSelStart();
443 
444             if (mSpan == null) mSpan = new BackgroundColorSpan(mDelegate.getHighlightColor());
445             SpannableString spanString = new SpannableString(state.getAutocompleteText());
446             // The flag here helps make sure that span does not get spill to other part of the text.
447             spanString.setSpan(mSpan, 0, state.getAutocompleteText().length(),
448                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
449             Editable editable = mDelegate.getEditableText();
450             editable.append(spanString);
451 
452             // Keep the original selection before adding spannable string.
453             Selection.setSelection(editable, sel, sel);
454             setCursorVisible(false);
455             if (DEBUG) Log.i(TAG, "setSpan: " + getEditableDebugString(editable));
456         }
457 
setCursorVisible(boolean visible)458         private void setCursorVisible(boolean visible) {
459             if (mDelegate.isFocused()) mDelegate.setCursorVisible(visible);
460         }
461 
getSpanIndex(Editable editable)462         private int getSpanIndex(Editable editable) {
463             if (editable == null || mSpan == null) return -1;
464             return editable.getSpanStart(mSpan); // returns -1 if mSpan is not attached
465         }
466 
reset()467         public void reset() {
468             setCursorVisible(true);
469             Editable editable = mDelegate.getEditableText();
470             int idx = getSpanIndex(editable);
471             if (idx != -1) {
472                 editable.removeSpan(mSpan);
473             }
474             mSpan = null;
475         }
476 
removeSpan()477         public boolean removeSpan() {
478             setCursorVisible(true);
479             Editable editable = mDelegate.getEditableText();
480             int idx = getSpanIndex(editable);
481             if (idx == -1) return false;
482             if (DEBUG) Log.i(TAG, "removeSpan IDX[%d]", idx);
483             editable.removeSpan(mSpan);
484             editable.delete(idx, editable.length());
485             mSpan = null;
486             if (DEBUG) {
487                 Log.i(TAG, "removeSpan - after removal: " + getEditableDebugString(editable));
488             }
489             return true;
490         }
491 
commitSpan()492         public void commitSpan() {
493             mDelegate.getEditableText().removeSpan(mSpan);
494             setCursorVisible(true);
495         }
496 
reflectTextUpdateInState(AutocompleteState state, CharSequence text)497         public void reflectTextUpdateInState(AutocompleteState state, CharSequence text) {
498             if (text instanceof Editable) {
499                 Editable editable = (Editable) text;
500                 int idx = getSpanIndex(editable);
501                 if (idx != -1) {
502                     // We do not set autocomplete text here as model should solely control it.
503                     state.setUserText(editable.subSequence(0, idx).toString());
504                     return;
505                 }
506             }
507             state.setUserText(text.toString());
508         }
509     }
510 
511     private class AutocompleteInputConnection extends InputConnectionWrapper {
512         private final AutocompleteState mPreBatchEditState;
513 
AutocompleteInputConnection()514         public AutocompleteInputConnection() {
515             super(null, true);
516             mPreBatchEditState = new AutocompleteState(mCurrentState);
517         }
518 
incrementBatchEditCount()519         private boolean incrementBatchEditCount() {
520             ++mBatchEditNestCount;
521             // After the outermost super.beginBatchEdit(), EditText will stop selection change
522             // update to the IME app.
523             return super.beginBatchEdit();
524         }
525 
decrementBatchEditCount()526         private boolean decrementBatchEditCount() {
527             --mBatchEditNestCount;
528             boolean retVal = super.endBatchEdit();
529             if (mBatchEditNestCount == 0) {
530                 // At the outermost super.endBatchEdit(), EditText will resume selection change
531                 // update to the IME app.
532                 updateSelectionForTesting();
533             }
534             return retVal;
535         }
536 
commitAutocomplete()537         public void commitAutocomplete() {
538             if (DEBUG) Log.i(TAG, "commitAutocomplete");
539             if (!hasAutocomplete()) return;
540 
541             String autocompleteText = mCurrentState.getAutocompleteText();
542 
543             mCurrentState.commitAutocompleteText();
544             // Invalidate mPreviouslySetState.
545             mPreviouslySetState.copyFrom(mCurrentState);
546             mLastEditWasTyping = false;
547 
548             if (mBatchEditNestCount == 0) {
549                 incrementBatchEditCount(); // avoids additional notifyAutocompleteTextStateChanged()
550                 mSpanCursorController.commitSpan();
551                 decrementBatchEditCount();
552             } else {
553                 // We have already removed span in the onBeginImeCommand(), just append the text.
554                 mDelegate.append(autocompleteText);
555             }
556         }
557 
558         @Override
beginBatchEdit()559         public boolean beginBatchEdit() {
560             if (DEBUG) Log.i(TAG, "beginBatchEdit");
561             onBeginImeCommand();
562             boolean retVal = incrementBatchEditCount();
563             onEndImeCommand();
564             return retVal;
565         }
566 
567         /**
568          * Always call this at the beginning of any IME command. Compare this with beginBatchEdit()
569          * which is by itself an IME command.
570          * @return Whether the call was successful.
571          */
onBeginImeCommand()572         public boolean onBeginImeCommand() {
573             if (DEBUG) Log.i(TAG, "onBeginImeCommand: " + mBatchEditNestCount);
574             boolean retVal = incrementBatchEditCount();
575             if (mBatchEditNestCount == 1) {
576                 mPreBatchEditState.copyFrom(mCurrentState);
577             } else if (mDeletePostfixOnNextBeginImeCommand > 0) {
578                 int len = mDelegate.getText().length();
579                 mDelegate.getText().delete(len - mDeletePostfixOnNextBeginImeCommand, len);
580             }
581             mDeletePostfixOnNextBeginImeCommand = 0;
582             mSpanCursorController.removeSpan();
583             return retVal;
584         }
585 
restoreBackspacedText(String diff)586         private void restoreBackspacedText(String diff) {
587             if (DEBUG) Log.i(TAG, "restoreBackspacedText. diff: " + diff);
588 
589             if (mBatchEditNestCount > 0) {
590                 // If batch edit hasn't finished, we will restore backspaced text only for visual
591                 // effects. However, for internal operations to work correctly, we need to remove
592                 // the restored diff at the beginning of next IME operation.
593                 mDeletePostfixOnNextBeginImeCommand = diff.length();
594             }
595             if (mBatchEditNestCount == 0) { // only at the outermost batch edit
596                 if (shouldFinishCompositionOnDeletion()) super.finishComposingText();
597             }
598             incrementBatchEditCount(); // avoids additional notifyAutocompleteTextStateChanged()
599             Editable editable = mDelegate.getEditableText();
600             editable.append(diff);
601             decrementBatchEditCount();
602         }
603 
setAutocompleteSpan()604         private boolean setAutocompleteSpan() {
605             mSpanCursorController.removeSpan();
606             if (DEBUG) {
607                 Log.i(TAG, "setAutocompleteSpan. %s->%s", mPreviouslySetState, mCurrentState);
608             }
609             if (!mCurrentState.isCursorAtEndOfUserText()) return false;
610 
611             if (mCurrentState.reuseAutocompleteTextIfPrefixExtension(mPreviouslySetState)) {
612                 mSpanCursorController.setSpan(mCurrentState);
613                 return true;
614             } else {
615                 return false;
616             }
617         }
618 
619         @Override
endBatchEdit()620         public boolean endBatchEdit() {
621             if (DEBUG) Log.i(TAG, "endBatchEdit");
622             onBeginImeCommand();
623             boolean retVal = decrementBatchEditCount();
624             onEndImeCommand();
625             return retVal;
626         }
627 
628         /**
629          * Always call this at the end of an IME command. Compare this with endBatchEdit()
630          * which is by itself an IME command.
631          * @return Whether the call was successful.
632          */
onEndImeCommand()633         public boolean onEndImeCommand() {
634             if (DEBUG) Log.i(TAG, "onEndImeCommand: " + (mBatchEditNestCount - 1));
635             String diff = mCurrentState.getBackwardDeletedTextFrom(mPreBatchEditState);
636             if (diff != null) {
637                 // Update selection first such that keyboard app gets what it expects.
638                 boolean retVal = decrementBatchEditCount();
639 
640                 if (mPreBatchEditState.hasAutocompleteText()) {
641                     // Undo delete to retain the last character and only remove autocomplete text.
642                     restoreBackspacedText(diff);
643                 }
644                 mLastEditWasTyping = false;
645                 clearAutocompleteText();
646                 notifyAutocompleteTextStateChanged();
647                 return retVal;
648             }
649             if (!setAutocompleteSpan()) {
650                 clearAutocompleteText();
651             }
652             boolean retVal = decrementBatchEditCount();
653             // Simply typed some characters or whole text selection has been overridden.
654             if (mCurrentState.isForwardTypedFrom(mPreBatchEditState)
655                     || (mPreBatchEditState.isWholeUserTextSelected()
656                                && mCurrentState.getUserText().length() > 0
657                                && mCurrentState.isCursorAtEndOfUserText())) {
658                 mLastEditWasTyping = true;
659             }
660             notifyAutocompleteTextStateChanged();
661             return retVal;
662         }
663 
664         @Override
commitText(CharSequence text, int newCursorPosition)665         public boolean commitText(CharSequence text, int newCursorPosition) {
666             if (DEBUG) Log.i(TAG, "commitText: " + text);
667             onBeginImeCommand();
668             boolean retVal = super.commitText(text, newCursorPosition);
669             onEndImeCommand();
670             return retVal;
671         }
672 
673         @Override
setComposingText(CharSequence text, int newCursorPosition)674         public boolean setComposingText(CharSequence text, int newCursorPosition) {
675             if (DEBUG) Log.i(TAG, "setComposingText: " + text);
676             onBeginImeCommand();
677             boolean retVal = super.setComposingText(text, newCursorPosition);
678             onEndImeCommand();
679             return retVal;
680         }
681 
682         @Override
setComposingRegion(int start, int end)683         public boolean setComposingRegion(int start, int end) {
684             if (DEBUG) Log.i(TAG, "setComposingRegion: [%d,%d]", start, end);
685             onBeginImeCommand();
686             boolean retVal = super.setComposingRegion(start, end);
687             onEndImeCommand();
688             return retVal;
689         }
690 
691         @Override
finishComposingText()692         public boolean finishComposingText() {
693             if (DEBUG) Log.i(TAG, "finishComposingText");
694             onBeginImeCommand();
695             boolean retVal = super.finishComposingText();
696             onEndImeCommand();
697             return retVal;
698         }
699 
700         @Override
deleteSurroundingText(final int beforeLength, final int afterLength)701         public boolean deleteSurroundingText(final int beforeLength, final int afterLength) {
702             if (DEBUG) Log.i(TAG, "deleteSurroundingText [%d,%d]", beforeLength, afterLength);
703             onBeginImeCommand();
704             boolean retVal = super.deleteSurroundingText(beforeLength, afterLength);
705             onEndImeCommand();
706             return retVal;
707         }
708 
709         @Override
setSelection(final int start, final int end)710         public boolean setSelection(final int start, final int end) {
711             if (DEBUG) Log.i(TAG, "setSelection [%d,%d]", start, end);
712             onBeginImeCommand();
713             boolean retVal = super.setSelection(start, end);
714             onEndImeCommand();
715             return retVal;
716         }
717 
718         @Override
performEditorAction(final int editorAction)719         public boolean performEditorAction(final int editorAction) {
720             if (DEBUG) Log.i(TAG, "performEditorAction: " + editorAction);
721             onBeginImeCommand();
722             commitAutocomplete();
723             boolean retVal = super.performEditorAction(editorAction);
724             onEndImeCommand();
725             return retVal;
726         }
727 
728         @Override
sendKeyEvent(final KeyEvent event)729         public boolean sendKeyEvent(final KeyEvent event) {
730             if (DEBUG) Log.i(TAG, "sendKeyEvent: " + event.getKeyCode());
731             onBeginImeCommand();
732             boolean retVal = super.sendKeyEvent(event);
733             onEndImeCommand();
734             return retVal;
735         }
736 
737         @Override
getExtractedText(final ExtractedTextRequest request, final int flags)738         public ExtractedText getExtractedText(final ExtractedTextRequest request, final int flags) {
739             if (DEBUG) Log.i(TAG, "getExtractedText");
740             onBeginImeCommand();
741             ExtractedText retVal = super.getExtractedText(request, flags);
742             onEndImeCommand();
743             return retVal;
744         }
745 
746         @Override
getTextAfterCursor(final int n, final int flags)747         public CharSequence getTextAfterCursor(final int n, final int flags) {
748             if (DEBUG) Log.i(TAG, "getTextAfterCursor");
749             onBeginImeCommand();
750             CharSequence retVal = super.getTextAfterCursor(n, flags);
751             onEndImeCommand();
752             return retVal;
753         }
754 
755         @Override
getTextBeforeCursor(final int n, final int flags)756         public CharSequence getTextBeforeCursor(final int n, final int flags) {
757             if (DEBUG) Log.i(TAG, "getTextBeforeCursor");
758             onBeginImeCommand();
759             CharSequence retVal = super.getTextBeforeCursor(n, flags);
760             onEndImeCommand();
761             return retVal;
762         }
763 
764         @Override
getSelectedText(final int flags)765         public CharSequence getSelectedText(final int flags) {
766             if (DEBUG) Log.i(TAG, "getSelectedText");
767             onBeginImeCommand();
768             CharSequence retVal = super.getSelectedText(flags);
769             onEndImeCommand();
770             return retVal;
771         }
772 
773         @Override
commitCompletion(CompletionInfo text)774         public boolean commitCompletion(CompletionInfo text) {
775             if (DEBUG) Log.i(TAG, "commitCompletion");
776             onBeginImeCommand();
777             boolean retVal = super.commitCompletion(text);
778             onEndImeCommand();
779             return retVal;
780         }
781 
782         @Override
commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts)783         public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
784             if (DEBUG) Log.i(TAG, "commitContent");
785             onBeginImeCommand();
786             boolean retVal = super.commitContent(inputContentInfo, flags, opts);
787             onEndImeCommand();
788             return retVal;
789         }
790 
791         @Override
commitCorrection(CorrectionInfo correctionInfo)792         public boolean commitCorrection(CorrectionInfo correctionInfo) {
793             if (DEBUG) Log.i(TAG, "commitCorrection");
794             onBeginImeCommand();
795             boolean retVal = super.commitCorrection(correctionInfo);
796             onEndImeCommand();
797             return retVal;
798         }
799 
800         @Override
deleteSurroundingTextInCodePoints(int beforeLength, int afterLength)801         public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
802             if (DEBUG) Log.i(TAG, "deleteSurroundingTextInCodePoints");
803             onBeginImeCommand();
804             boolean retVal = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
805             onEndImeCommand();
806             return retVal;
807         }
808 
809         @Override
getCursorCapsMode(int reqModes)810         public int getCursorCapsMode(int reqModes) {
811             if (DEBUG) Log.i(TAG, "getCursorCapsMode");
812             onBeginImeCommand();
813             int retVal = super.getCursorCapsMode(reqModes);
814             onEndImeCommand();
815             return retVal;
816         }
817 
818         @Override
requestCursorUpdates(int cursorUpdateMode)819         public boolean requestCursorUpdates(int cursorUpdateMode) {
820             if (DEBUG) Log.i(TAG, "requestCursorUpdates");
821             onBeginImeCommand();
822             boolean retVal = super.requestCursorUpdates(cursorUpdateMode);
823             onEndImeCommand();
824             return retVal;
825         }
826 
827         @Override
clearMetaKeyStates(int states)828         public boolean clearMetaKeyStates(int states) {
829             if (DEBUG) Log.i(TAG, "clearMetaKeyStates");
830             onBeginImeCommand();
831             boolean retVal = super.clearMetaKeyStates(states);
832             onEndImeCommand();
833             return retVal;
834         }
835     }
836 }
837