Merge "Add SubtypeLocaleUtils.isRtlLanguage method"
diff --git a/java/res/values-ne-rNP/strings.xml b/java/res/values-ne-rNP/strings.xml
index c4792dc..168806e 100644
--- a/java/res/values-ne-rNP/strings.xml
+++ b/java/res/values-ne-rNP/strings.xml
@@ -76,7 +76,7 @@
     <string name="gesture_floating_preview_text" msgid="4443240334739381053">"गतिशील फ्लोटिङ पूर्वावलोकन"</string>
     <string name="gesture_floating_preview_text_summary" msgid="4472696213996203533">"इशारा गर्दा सुझाव दिइएको शब्द हेर्नुहोस्"</string>
     <string name="gesture_space_aware" msgid="2078291600664682496">"वाक्यांश इशारा"</string>
-    <string name="gesture_space_aware_summary" msgid="4371385818348528538">"इशारा सत्र दैरान space दिखिल गर्न space key मा ग्लाइडि गर्नुहोस्"</string>
+    <string name="gesture_space_aware_summary" msgid="4371385818348528538">"इशाराको बखतमा स्पेस कुञ्जीमा ग्लाईडिंग द्वारा आगत खाली ठाउँहरू"</string>
     <string name="added_word" msgid="8993883354622484372">"<xliff:g id="WORD">%s</xliff:g> : बचत गरियो"</string>
     <string name="spoken_use_headphones" msgid="896961781287283493">"हेडसेट प्लग इन गर्नुहोस्"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"वर्तमान पाठ %s हो"</string>
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index 10fb9fe..216a825 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -158,7 +158,7 @@
      * @param typedWord the currently typed word
      */
     public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
-        if (suggestedWords != null && suggestedWords.mWillAutoCorrect) {
+        if (suggestedWords.mWillAutoCorrect) {
             mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
             mTypedWord = typedWord;
         } else {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 82d824b..ba7503d 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -789,7 +789,7 @@
         if (mSuggestionStripView != null) {
             // This will set the punctuation suggestions if next word suggestion is off;
             // otherwise it will clear the suggestion strip.
-            setPunctuationSuggestions();
+            setNeutralSuggestionStrip();
         }
 
         // Sometimes, while rotating, for some reason the framework tells the app we are not
@@ -912,63 +912,14 @@
                     composingSpanEnd, mInputLogic.mConnection);
         }
 
-        final boolean selectionChanged = oldSelStart != newSelStart || oldSelEnd != newSelEnd;
-
-        // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
-        // span in the view - we can use that to narrow down whether the cursor was moved
-        // by us or not. If we are composing a word but there is no composing span, then
-        // we know for sure the cursor moved while we were composing and we should reset
-        // the state. TODO: rescind this policy: the framework never removes the composing
-        // span on its own accord while editing. This test is useless.
-        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
-
         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
         // will be reset when the keyboard shows up anyway.
         // TODO: revisit this when LatinIME supports hardware keyboards.
         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
         // TODO: find a better way to simulate actual execution.
-        if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart,
-                newSelStart, oldSelEnd, newSelEnd)) {
-            // TODO: the following is probably better done in resetEntireInputState().
-            // it should only happen when the cursor moved, and the very purpose of the
-            // test below is to narrow down whether this happened or not. Likewise with
-            // the call to updateShiftState.
-            // We set this to NONE because after a cursor move, we don't want the space
-            // state-related special processing to kick in.
-            mInputLogic.mSpaceState = SpaceState.NONE;
-
-            // TODO: is it still necessary to test for composingSpan related stuff?
-            final boolean selectionChangedOrSafeToReset = selectionChanged
-                    || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan;
-            final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
-                    || newSelStart != newSelEnd);
-            final int moveAmount = newSelStart - oldSelStart;
-            if (selectionChangedOrSafeToReset && (hasOrHadSelection
-                    || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord(
-                            moveAmount))) {
-                // If we are composing a word and moving the cursor, we would want to set a
-                // suggestion span for recorrection to work correctly. Unfortunately, that
-                // would involve the keyboard committing some new text, which would move the
-                // cursor back to where it was. Latin IME could then fix the position of the cursor
-                // again, but the asynchronous nature of the calls results in this wreaking havoc
-                // with selection on double tap and the like.
-                // Another option would be to send suggestions each time we set the composing
-                // text, but that is probably too expensive to do, so we decided to leave things
-                // as is.
-                mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd);
-            } else {
-                // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
-                // composition to end.  But in all cases where we don't reset the entire input
-                // state, we still want to tell the rich input connection about the new cursor
-                // position so that it can update its caches.
-                mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
-                        newSelStart, newSelEnd, false /* shouldFinishComposition */);
-            }
-
-            // We moved the cursor. If we are touching a word, we need to resume suggestion.
-            mHandler.postResumeSuggestions();
-            // Reset the last recapitalization.
-            mInputLogic.mRecapitalizeStatus.deactivate();
+        if (isInputViewShown() &&
+                mInputLogic.onUpdateSelection(mSettings.getCurrent(), oldSelStart, oldSelEnd,
+                        newSelStart, newSelEnd, composingSpanStart, composingSpanEnd)) {
             mKeyboardSwitcher.updateShiftState();
         }
 
@@ -1039,7 +990,7 @@
         }
         if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return;
         if (applicationSpecifiedCompletions == null) {
-            clearSuggestionStrip();
+            setNeutralSuggestionStrip();
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.latinIME_onDisplayCompletions(null);
             }
@@ -1054,7 +1005,7 @@
         final SuggestedWords suggestedWords = new SuggestedWords(
                 applicationSuggestedWords,
                 false /* typedWordValid */,
-                false /* hasAutoCorrectionCandidate */,
+                false /* willAutoCorrect */,
                 false /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
                 false /* isPrediction */);
@@ -1183,7 +1134,7 @@
     // the right layout.
     // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
     public int getCurrentAutoCapsState() {
-        return mInputLogic.getCurrentAutoCapsState(Settings.getInstance().getCurrent());
+        return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent());
     }
 
     // Called from the KeyboardSwitcher which needs to know recaps state to display
@@ -1289,7 +1240,7 @@
             mSubtypeSwitcher.switchToShortcutIME(this);
             // Still call the *#onCodeInput methods for readability.
         }
-        mInputLogic.onCodeInput(codeToSend, keyX, keyY, Settings.getInstance().getCurrent(),
+        mInputLogic.onCodeInput(codeToSend, keyX, keyY, mSettings.getCurrent(),
                 mHandler, mKeyboardSwitcher);
         mKeyboardSwitcher.onCodeInput(codePoint);
     }
@@ -1379,12 +1330,6 @@
     }
 
     // TODO[IL]: Define a clear interface for this
-    public void clearSuggestionStrip() {
-        setSuggestedWords(SuggestedWords.EMPTY);
-        setAutoCorrectionIndicator(false);
-    }
-
-    // TODO[IL]: Define a clear interface for this
     public void setSuggestedWords(final SuggestedWords words) {
         mInputLogic.mSuggestedWords = words;
         if (mSuggestionStripView != null) {
@@ -1479,12 +1424,16 @@
         }
     }
 
-    private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
+    private void showSuggestionStripWithTypedWord(final SuggestedWords sourceSuggestedWords,
             final String typedWord) {
+        // TODO: refactor this
+        final SuggestedWords suggestedWords =
+                sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords;
         if (suggestedWords.isEmpty()) {
             // No auto-correction is available, clear the cached values.
-            AccessibilityUtils.getInstance().setAutoCorrection(null, null);
-            clearSuggestionStrip();
+            AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
+            setSuggestedWords(suggestedWords);
+            setAutoCorrectionIndicator(false);
             return;
         }
         final String autoCorrection;
@@ -1506,12 +1455,8 @@
 
     // TODO[IL]: Define a clean interface for this
     public void showSuggestionStrip(final SuggestedWords suggestedWords) {
-        if (suggestedWords.isEmpty()) {
-            clearSuggestionStrip();
-            return;
-        }
-        showSuggestionStripWithTypedWord(suggestedWords,
-                suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
+        showSuggestionStripWithTypedWord(suggestedWords, suggestedWords.isEmpty() ? null
+                : suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
     }
 
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
@@ -1610,10 +1555,12 @@
     }
 
     // TODO[IL]: Define a clean interface for this
-    public void setPunctuationSuggestions() {
+    // This will show either an empty suggestion strip (if prediction is enabled) or
+    // punctuation suggestions (if it's disabled).
+    public void setNeutralSuggestionStrip() {
         final SettingsValues currentSettings = mSettings.getCurrent();
         if (currentSettings.mBigramPredictionEnabled) {
-            clearSuggestionStrip();
+            setSuggestedWords(SuggestedWords.EMPTY);
         } else {
             setSuggestedWords(currentSettings.mSpacingAndPunctuations.mSuggestPuncList);
         }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 325a0d9..0d0b7a1 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -815,6 +815,17 @@
     }
 
     /**
+     * Looks at the text just before the cursor to find out if we are inside a double quote.
+     *
+     * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached.
+     * However this won't be a concrete problem in most situations, as the cache is almost always
+     * long enough for this use.
+     */
+    public boolean isInsideDoubleQuoteOrAfterDigit() {
+        return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText);
+    }
+
+    /**
      * Try to get the text from the editor to expose lies the framework may have been
      * telling us. Concretely, when the device rotates, the frameworks tells us about where the
      * cursor used to be initially in the editor at the time it first received the focus; this
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 6e35432..43d7533 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -179,6 +179,67 @@
     }
 
     /**
+     * Consider an update to the cursor position. Evaluate whether this update has happened as
+     * part of normal typing or whether it was an explicit cursor move by the user. In any case,
+     * do the necessary adjustments.
+     * @param settingsValues the current settings
+     * @param oldSelStart old selection start
+     * @param oldSelEnd old selection end
+     * @param newSelStart new selection start
+     * @param newSelEnd new selection end
+     * @param composingSpanStart composing span start
+     * @param composingSpanEnd composing span end
+     * @return whether the cursor has moved as a result of user interaction.
+     */
+    public boolean onUpdateSelection(final SettingsValues settingsValues,
+            final int oldSelStart, final int oldSelEnd,
+            final int newSelStart, final int newSelEnd,
+            final int composingSpanStart, final int composingSpanEnd) {
+        if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) {
+            return false;
+        }
+        // TODO: the following is probably better done in resetEntireInputState().
+        // it should only happen when the cursor moved, and the very purpose of the
+        // test below is to narrow down whether this happened or not. Likewise with
+        // the call to updateShiftState.
+        // We set this to NONE because after a cursor move, we don't want the space
+        // state-related special processing to kick in.
+        mSpaceState = SpaceState.NONE;
+
+        final boolean selectionChangedOrSafeToReset =
+                oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed
+                || !mWordComposer.isComposingWord(); // safe to reset
+        final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd);
+        final int moveAmount = newSelStart - oldSelStart;
+        if (selectionChangedOrSafeToReset && (hasOrHadSelection
+                || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
+            // If we are composing a word and moving the cursor, we would want to set a
+            // suggestion span for recorrection to work correctly. Unfortunately, that
+            // would involve the keyboard committing some new text, which would move the
+            // cursor back to where it was. Latin IME could then fix the position of the cursor
+            // again, but the asynchronous nature of the calls results in this wreaking havoc
+            // with selection on double tap and the like.
+            // Another option would be to send suggestions each time we set the composing
+            // text, but that is probably too expensive to do, so we decided to leave things
+            // as is.
+            resetEntireInputState(settingsValues, newSelStart, newSelEnd);
+        } else {
+            // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
+            // composition to end. But in all cases where we don't reset the entire input
+            // state, we still want to tell the rich input connection about the new cursor
+            // position so that it can update its caches.
+            mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+                    newSelStart, newSelEnd, false /* shouldFinishComposition */);
+        }
+
+        // We moved the cursor. If we are touching a word, we need to resume suggestion.
+        mLatinIME.mHandler.postResumeSuggestions();
+        // Reset the last recapitalization.
+        mRecapitalizeStatus.deactivate();
+        return true;
+    }
+
+    /**
      * React to a code input. It may be a code point to insert, or a symbolic value that influences
      * the keyboard behavior.
      *
@@ -600,8 +661,21 @@
         final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
                 isFromSuggestionStrip);
 
-        if (SpaceState.PHANTOM == spaceState &&
-                settingsValues.isUsuallyPrecededBySpace(codePoint)) {
+        final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
+                && mConnection.isInsideDoubleQuoteOrAfterDigit();
+
+        final boolean needsPrecedingSpace;
+        if (SpaceState.PHANTOM != spaceState) {
+            needsPrecedingSpace = false;
+        } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
+            // Double quotes behave like they are usually preceded by space iff we are
+            // not inside a double quote or after a digit.
+            needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit;
+        } else {
+            needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint);
+        }
+
+        if (needsPrecedingSpace) {
             promotePhantomSpace(settingsValues);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -628,14 +702,17 @@
             if (swapWeakSpace) {
                 swapSwapperAndSpace(keyboardSwitcher);
                 mSpaceState = SpaceState.SWAP_PUNCTUATION;
-            } else if (SpaceState.PHANTOM == spaceState
-                    && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
+            } else if ((SpaceState.PHANTOM == spaceState
+                    && settingsValues.isUsuallyFollowedBySpace(codePoint))
+                    || (Constants.CODE_DOUBLE_QUOTE == codePoint
+                            && isInsideDoubleQuoteOrAfterDigit)) {
                 // If we are in phantom space state, and the user presses a separator, we want to
                 // stay in phantom space state so that the next keypress has a chance to add the
                 // space. For example, if I type "Good dat", pick "day" from the suggestion strip
                 // then insert a comma and go on to typing the next word, I want the space to be
                 // inserted automatically before the next word, the same way it is when I don't
-                // input the comma.
+                // input the comma. A double quote behaves like it's usually followed by space if
+                // we're inside a double quote.
                 // The case is a little different if the separator is a space stripper. Such a
                 // separator does not normally need a space on the right (that's the difference
                 // between swappers and strippers), so we should not stay in phantom space state if
@@ -645,7 +722,7 @@
 
             // Set punctuation right away. onUpdateSelection will fire but tests whether it is
             // already displayed or not, so it's okay.
-            mLatinIME.setPunctuationSuggestions();
+            mLatinIME.setNeutralSuggestionStrip();
         }
 
         keyboardSwitcher.updateShiftState();
@@ -998,7 +1075,7 @@
         }
 
         if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) {
-            mLatinIME.setPunctuationSuggestions();
+            mLatinIME.setNeutralSuggestionStrip();
             return;
         }
 
@@ -1383,11 +1460,7 @@
             final int newSelStart, final int newSelEnd) {
         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
         resetComposingState(true /* alsoResetLastComposedWord */);
-        if (settingsValues.mBigramPredictionEnabled) {
-            mLatinIME.clearSuggestionStrip();
-        } else {
-            mLatinIME.setSuggestedWords(settingsValues.mSpacingAndPunctuations.mSuggestPuncList);
-        }
+        mLatinIME.setNeutralSuggestionStrip();
         mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
                 shouldFinishComposition);
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 6f15b11..b154623 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -348,7 +348,7 @@
         boolean hasPeriod = false;
         int codePoint = 0;
         while (i > 0) {
-            codePoint =  Character.codePointBefore(text, i);
+            codePoint = Character.codePointBefore(text, i);
             if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') {
                 // Handwavy heuristic to see if that's a URL character. Anything between period
                 // and z. This includes all lower- and upper-case ascii letters, period,
@@ -387,6 +387,48 @@
         return false;
     }
 
+    /**
+     * Examines the string and returns whether we're inside a double quote.
+     *
+     * This is used to decide whether we should put an automatic space before or after a double
+     * quote character. If we're inside a quotation, then we want to close it, so we want a space
+     * after and not before. Otherwise, we want to open the quotation, so we want a space before
+     * and not after. Exception: after a digit, we never want a space because the "inch" or
+     * "minutes" use cases is dominant after digits.
+     * In the practice, we determine whether we are in a quotation or not by finding the previous
+     * double quote character, and looking at whether it's followed by whitespace. If so, that
+     * was a closing quotation mark, so we're not inside a double quote. If it's not followed
+     * by whitespace, then it was an opening quotation mark, and we're inside a quotation.
+     *
+     * @param text the text to examine.
+     * @return whether we're inside a double quote.
+     */
+    public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) {
+        int i = text.length();
+        if (0 == i) return false;
+        int codePoint = Character.codePointBefore(text, i);
+        if (Character.isDigit(codePoint)) return true;
+        int prevCodePoint = 0;
+        while (i > 0) {
+            codePoint = Character.codePointBefore(text, i);
+            if (Constants.CODE_DOUBLE_QUOTE == codePoint) {
+                // If we see a double quote followed by whitespace, then that
+                // was a closing quote.
+                if (Character.isWhitespace(prevCodePoint)) return false;
+            }
+            if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) {
+                // If we see a double quote preceded by whitespace, then that
+                // was an opening quote. No need to continue seeking.
+                return true;
+            }
+            i -= Character.charCount(codePoint);
+            prevCodePoint = codePoint;
+        }
+        // We reached the start of text. If the first char is a double quote, then we're inside
+        // a double quote. Otherwise we're not.
+        return Constants.CODE_DOUBLE_QUOTE == codePoint;
+    }
+
     public static boolean isEmptyStringOrWhiteSpaces(final String s) {
         final int N = codePointCount(s);
         for (int i = 0; i < N; ++i) {
diff --git a/tests/src/com/android/inputmethod/latin/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index d5c06e2..556af09 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -169,4 +169,32 @@
                 + " ; Suggestions = " + mLatinIME.getSuggestedWords(),
                 EXPECTED_RESULT, mEditText.getText().toString());
     }
+
+    public void testAutoSpaceWithDoubleQuotes() {
+        final String STRING_TO_TYPE = "He said\"hello\"to me. I replied,\"hi\"."
+                + "Then, 5\"passed. He said\"bye\"and left.";
+        final String EXPECTED_RESULT = "He said \"hello\" to me. I replied, \"hi\". "
+                + "Then, 5\" passed. He said \"bye\" and left. \"";
+        // Split by double quote, so that we can type the double quotes individually.
+        for (final String partToType : STRING_TO_TYPE.split("\"")) {
+            // Split at word boundaries. This regexp means "anywhere that is preceded
+            // by a word character but not followed by a word character, OR that is not
+            // preceded by a word character but followed by a word character".
+            // We need to input word by word because auto-spaces are only active when
+            // manually picking or gesturing (which we can't simulate yet), but only words
+            // can be picked.
+            final String[] wordsToType = partToType.split("(?<=\\w)(?!\\w)|(?<!\\w)(?=\\w)");
+            for (final String wordToType : wordsToType) {
+                type(wordToType);
+                if (wordToType.matches("^\\w+$")) {
+                    // Only pick selection if that was a word, because if that was not a word,
+                    // then we don't have a composition.
+                    pickSuggestionManually(0, wordToType);
+                }
+            }
+            type("\"");
+        }
+        assertEquals("auto-space with double quotes",
+                EXPECTED_RESULT, mEditText.getText().toString());
+    }
 }