Introduce a mechanism to hide the indicator speculatively

This is an optional optimization to reduce the UI latency.

Imagine that the commit indicator is now displayed and the
composing text is being updated, it is highly likely that
the commit indicator will disappear unless the application
rejects the setComposingText request.

If we assume that the application will accept the new
composing text without any modifications, we can hide the
indicator without waiting for the arrival of new
CursorAnchorInfo event.

This optimization isn't dangerous because we can show the
indicator again when we receive new CursorAnchorInfo event
and the assumption is turned out to be invalid.

Change-Id: Id59c6607a1029782410611e768791af9984f14ac
diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java
index 0eb8b44..1785161 100644
--- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java
+++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java
@@ -158,7 +158,7 @@
         if (!currentFullScreenMode && fullScreenMode) {
             // Currently full screen mode is not supported.
             // TODO: Support full screen mode.
-            hideIndicator();
+            mUiOperator.hideUi();
         }
         mIsFullScreenMode = fullScreenMode;
     }
@@ -193,17 +193,36 @@
         layoutImmediately();
     }
 
-    private void hideIndicator() {
-        mUiOperator.hideUi();
+    /**
+     * Hides indicator if the new composing text doesn't match the expected one.
+     *
+     * <p>Calling this method is optional but recommended whenever the new composition is passed to
+     * the application. The motivation of this method is to reduce the UI latency. With this method,
+     * we can hide the indicator without waiting the arrival of the
+     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} callback, assuming that
+     * the application accepts the new composing text without any modification. Even if this
+     * assumption is false, the indicator will be shown again when
+     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is actually received.
+     * </p>
+     *
+     * @param newComposingText the new composing text that is being passed to the application.
+     */
+    public void hideIndicatorIfNecessary(final CharSequence newComposingText) {
+        if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) {
+            return;
+        }
+        if (!TextUtils.equals(newComposingText, mWaitingWord.mWord)) {
+            mUiOperator.hideUi();
+        }
     }
 
     private void cancelLayoutInternalUnexpectedly(final String message) {
-        hideIndicator();
+        mUiOperator.hideUi();
         Log.d(TAG, message);
     }
 
     private void cancelLayoutInternalExpectedly(final String message) {
-        hideIndicator();
+        mUiOperator.hideUi();
         if (DEBUG) {
             Log.d(TAG, message);
         }
@@ -261,7 +280,7 @@
                     lastCharRectFlag & CursorAnchorInfoCompatWrapper.CHARACTER_RECT_TYPE_MASK;
             if (lastCharRect == null || matrix == null || lastCharRectType !=
                     CursorAnchorInfoCompatWrapper.CHARACTER_RECT_TYPE_FULLY_VISIBLE) {
-                hideIndicator();
+                mUiOperator.hideUi();
                 return;
             }
             final RectF segmentStartCharRect = new RectF(lastCharRect);
@@ -312,13 +331,13 @@
             if (!TextUtils.isEmpty(composingText)) {
                 // This is an unexpected case.
                 // TODO: Document this.
-                hideIndicator();
+                mUiOperator.hideUi();
                 return;
             }
             // In MODE_ADD_TO_DICTIONARY, we cannot retrieve the character position at all because
             // of the lack of composing text. We will use the insertion marker position instead.
             if (info.isInsertionMarkerClipped()) {
-                hideIndicator();
+                mUiOperator.hideUi();
                 return;
             }
             final float insertionMarkerHolizontal = info.getInsertionMarkerHorizontal();
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 0f2ba53..e83f494 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -649,7 +649,7 @@
             // message, this is called outside any batch edit. Potentially, this may result in some
             // janky flickering of the screen, although the display speed makes it unlikely in
             // the practice.
-            mConnection.setComposingText(textWithUnderline, 1);
+            setComposingTextInternal(textWithUnderline, 1);
         }
     }
 
@@ -672,7 +672,7 @@
             inputTransaction.setDidAffectContents();
         }
         if (mWordComposer.isComposingWord()) {
-            mConnection.setComposingText(mWordComposer.getTypedWord(), 1);
+            setComposingTextInternal(mWordComposer.getTypedWord(), 1);
             inputTransaction.setDidAffectContents();
             inputTransaction.setRequiresUpdateSuggestions();
         }
@@ -908,8 +908,7 @@
             if (mWordComposer.isSingleLetter()) {
                 mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
             }
-            mConnection.setComposingText(getTextWithUnderline(
-                    mWordComposer.getTypedWord()), 1);
+            setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
         } else {
             final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
                     inputTransaction);
@@ -1072,7 +1071,7 @@
                 mWordComposer.applyProcessedEvent(event);
             }
             if (mWordComposer.isComposingWord()) {
-                mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+                setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
             } else {
                 mConnection.commitText("", 1);
             }
@@ -1640,7 +1639,7 @@
             final int[] codePoints = StringUtils.toCodePointArray(stringToCommit);
             mWordComposer.setComposingWord(codePoints,
                     mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
-            mConnection.setComposingText(textToCommit, 1);
+            setComposingTextInternal(textToCommit, 1);
         }
         // Don't restart suggestion yet. We'll restart if the user deletes the separator.
         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
@@ -1973,10 +1972,10 @@
             }
             final String lastWord = batchInputText.substring(indexOfLastSpace);
             mWordComposer.setBatchInputWord(lastWord);
-            mConnection.setComposingText(lastWord, 1);
+            setComposingTextInternal(lastWord, 1);
         } else {
             mWordComposer.setBatchInputWord(batchInputText);
-            mConnection.setComposingText(batchInputText, 1);
+            setComposingTextInternal(batchInputText, 1);
         }
         mConnection.endBatchEdit();
         // Space state must be updated before calling updateShiftState
@@ -2175,6 +2174,24 @@
                 inputStyle, sequenceNumber, callback);
     }
 
+    /**
+     * Used as an injection point for each call of
+     * {@link RichInputConnection#setComposingText(CharSequence, int)}.
+     *
+     * <p>Currently using this method is optional and you can still directly call
+     * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to
+     * use this method whenever possible to optimize the behavior of {@link TextDecorator}.<p>
+     * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p>
+     *
+     * @param newComposingText the composing text to be set
+     * @param newCursorPosition the new cursor position
+     */
+    private void setComposingTextInternal(final CharSequence newComposingText,
+            final int newCursorPosition) {
+        mConnection.setComposingText(newComposingText, newCursorPosition);
+        mTextDecorator.hideIndicatorIfNecessary(newComposingText);
+    }
+
     //////////////////////////////////////////////////////////////////////////////////////////////
     // Following methods are tentatively placed in this class for the integration with
     // TextDecorator.