Merge "Fix unit tests by adding @UsedForTesting"
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 00b54f5..77e99bf 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -183,6 +183,7 @@
     public static final int CODE_TAB = '\t';
     public static final int CODE_SPACE = ' ';
     public static final int CODE_PERIOD = '.';
+    public static final int CODE_COMMA = ',';
     public static final int CODE_ARMENIAN_PERIOD = 0x0589;
     public static final int CODE_DASH = '-';
     public static final int CODE_SINGLE_QUOTE = '\'';
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index fd2cd30..3fca4fd 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -213,7 +213,7 @@
                 postUpdateSuggestionStrip();
                 break;
             case MSG_ON_END_BATCH_INPUT:
-                latinIme.mInputLogic.endBatchInputAsyncInternal(latinIme.mSettings.getCurrent(),
+                latinIme.mInputLogic.endBatchInputInternal(latinIme.mSettings.getCurrent(),
                         (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher);
                 break;
             case MSG_RESET_CACHES:
@@ -234,6 +234,9 @@
         }
 
         public void postResumeSuggestions() {
+            if (!getOwnerInstance().mSettings.getCurrent().isSuggestionStripVisible()) {
+                return;
+            }
             removeMessages(MSG_RESUME_SUGGESTIONS);
             sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
         }
@@ -959,11 +962,8 @@
                         newSelStart, newSelEnd, false /* shouldFinishComposition */);
             }
 
-            // We moved the cursor. If we are touching a word, we need to resume suggestion,
-            // unless suggestions are off.
-            if (isSuggestionsStripVisible()) {
-                mHandler.postResumeSuggestions();
-            }
+            // We moved the cursor. If we are touching a word, we need to resume suggestion.
+            mHandler.postResumeSuggestions();
             // Reset the last recapitalization.
             mInputLogic.mRecapitalizeStatus.deactivate();
             mKeyboardSwitcher.updateShiftState();
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 79d6674..a5b147a 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -27,7 +27,6 @@
 import android.view.inputmethod.InputConnection;
 
 import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.settings.SettingsValues;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
@@ -98,7 +97,7 @@
         final ExtractedText et = mIC.getExtractedText(r, 0);
         final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
                 0);
-        final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
+        final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText)
                 .append(mComposingText);
         if (null == et || null == beforeCursor) return;
         final int actualLength = Math.min(beforeCursor.length(), internal.length());
@@ -253,8 +252,7 @@
     }
 
     public CharSequence getSelectedText(final int flags) {
-        if (null == mIC) return null;
-        return mIC.getSelectedText(flags);
+        return (null == mIC) ? null : mIC.getSelectedText(flags);
     }
 
     public boolean canDeleteCharacters() {
@@ -272,12 +270,12 @@
      * American English, it's just the most common set of rules for English).
      *
      * @param inputType a mask of the caps modes to test for.
-     * @param settingsValues the values of the settings to use for locale and separators.
+     * @param spacingAndPunctuations the values of the settings to use for locale and separators.
      * @param hasSpaceBefore if we should consider there should be a space after the string.
      * @return the caps modes that should be on as a set of bits
      */
-    public int getCursorCapsMode(final int inputType, final SettingsValues settingsValues,
-            final boolean hasSpaceBefore) {
+    public int getCursorCapsMode(final int inputType,
+            final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
         mIC = mParent.getCurrentInputConnection();
         if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
         if (!TextUtils.isEmpty(mComposingText)) {
@@ -304,13 +302,13 @@
         // This never calls InputConnection#getCapsMode - in fact, it's a static method that
         // never blocks or initiates IPC.
         return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
-                settingsValues.mSpacingAndPunctuations, hasSpaceBefore);
+                spacingAndPunctuations, hasSpaceBefore);
     }
 
     public int getCodePointBeforeCursor() {
-        if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE;
-        return Character.codePointBefore(mCommittedTextBeforeComposingText,
-                mCommittedTextBeforeComposingText.length());
+        final int length = mCommittedTextBeforeComposingText.length();
+        if (length < 1) return Constants.NOT_A_CODE;
+        return Character.codePointBefore(mCommittedTextBeforeComposingText, length);
     }
 
     public CharSequence getTextBeforeCursor(final int n, final int flags) {
@@ -338,16 +336,12 @@
             return s;
         }
         mIC = mParent.getCurrentInputConnection();
-        if (null != mIC) {
-            return mIC.getTextBeforeCursor(n, flags);
-        }
-        return null;
+        return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags);
     }
 
     public CharSequence getTextAfterCursor(final int n, final int flags) {
         mIC = mParent.getCurrentInputConnection();
-        if (null != mIC) return mIC.getTextAfterCursor(n, flags);
-        return null;
+        return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags);
     }
 
     public void deleteSurroundingText(final int beforeLength, final int afterLength) {
@@ -681,23 +675,28 @@
                         startIndexInBefore, before.length() + endIndexInAfter, before.length());
     }
 
-    public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+    public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) {
         final int codePointBeforeCursor = getCodePointBeforeCursor();
-        if (Constants.NOT_A_CODE != codePointBeforeCursor
-                && !settingsValues.isWordSeparator(codePointBeforeCursor)
-                && !settingsValues.isWordConnector(codePointBeforeCursor)) {
-            return true;
+        if (Constants.NOT_A_CODE == codePointBeforeCursor
+                || spacingAndPunctuations.isWordSeparator(codePointBeforeCursor)
+                || spacingAndPunctuations.isWordConnector(codePointBeforeCursor)) {
+            return isCursorFollowedByWordCharacter(spacingAndPunctuations);
         }
-        return isCursorFollowedByWordCharacter(settingsValues);
+        return true;
     }
 
-    public boolean isCursorFollowedByWordCharacter(final SettingsValues settingsValues) {
+    public boolean isCursorFollowedByWordCharacter(
+            final SpacingAndPunctuations spacingAndPunctuations) {
         final CharSequence after = getTextAfterCursor(1, 0);
-        if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
-                && !settingsValues.isWordConnector(after.charAt(0))) {
-            return true;
+        if (TextUtils.isEmpty(after)) {
+            return false;
         }
-        return false;
+        final int codePointAfterCursor = Character.codePointAt(after, 0);
+        if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor)
+                || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) {
+            return false;
+        }
+        return true;
     }
 
     public void removeTrailingSpace() {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index beef761..3ff2079 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -530,7 +530,7 @@
                 && settingsValues.isSuggestionsRequested() &&
         // In languages with spaces, we only start composing a word when we are not already
         // touching a word. In languages without spaces, the above conditions are sufficient.
-                (!mConnection.isCursorTouchingWord(settingsValues)
+                (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)
                         || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) {
             // Reset entirely the composing state anyway, then start composing a new word unless
             // the character is a single quote or a dash. The idea here is, single quote and dash
@@ -816,7 +816,8 @@
             }
             if (settingsValues.isSuggestionStripVisible()
                     && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
-                    && !mConnection.isCursorFollowedByWordCharacter(settingsValues)) {
+                    && !mConnection.isCursorFollowedByWordCharacter(
+                            settingsValues.mSpacingAndPunctuations)) {
                 restartSuggestionsOnWordTouchedByCursor(settingsValues,
                         true /* includeResumedWordInSuggestions */, keyboardSwitcher);
             }
@@ -1061,8 +1062,6 @@
         // recorrection. This is a temporary, stopgap measure that will be removed later.
         // TODO: remove this.
         if (settingsValues.isBrokenByRecorrection()) return;
-        // A simple way to test for support from the TextView.
-        if (!mLatinIME.isSuggestionsStripVisible()) return;
         // Recorrection is not supported in languages without spaces because we don't know
         // how to segment them yet.
         if (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) return;
@@ -1071,7 +1070,7 @@
         // If we don't know the cursor location, return.
         if (mConnection.getExpectedSelectionStart() < 0) return;
         final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
-        if (!mConnection.isCursorTouchingWord(settingsValues)) return;
+        if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) return;
         final TextRange range = mConnection.getWordRangeAtCursor(
                 settingsValues.mSpacingAndPunctuations.mWordSeparators,
                 0 /* additionalPrecedingWordsCount */);
@@ -1292,7 +1291,7 @@
         final int inputType = ei.inputType;
         // Warning: this depends on mSpaceState, which may not be the most current value. If
         // mSpaceState gets updated later, whoever called this may need to be told about it.
-        return mConnection.getCursorCapsMode(inputType, settingsValues,
+        return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations,
                 SpaceState.PHANTOM == mSpaceState);
     }
 
@@ -1536,7 +1535,7 @@
      * @param settingsValues the current values of the settings.
      * @param suggestedWords suggestedWords to use.
      */
-    public void endBatchInputAsyncInternal(final SettingsValues settingsValues,
+    public void endBatchInputInternal(final SettingsValues settingsValues,
             final SuggestedWords suggestedWords,
             // TODO: remove this argument
             final KeyboardSwitcher keyboardSwitcher) {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
index ea010b6..b09e205 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
@@ -29,8 +29,7 @@
 /**
  * A helper to manage deferred tasks for the input logic.
  */
-// TODO: Make this package private
-public class InputLogicHandler implements Handler.Callback {
+class InputLogicHandler implements Handler.Callback {
     final Handler mNonUIThreadHandler;
     // TODO: remove this reference.
     final LatinIME mLatinIME;
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index dbe30e2..29bd3e7 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -41,6 +41,7 @@
     public final String mSentenceSeparatorAndSpace;
     public final boolean mCurrentLanguageHasSpaces;
     public final boolean mUsesAmericanTypography;
+    public final boolean mUsesGermanRules;
 
     public SpacingAndPunctuations(final Resources res) {
         mSymbolsPrecededBySpace =
@@ -62,8 +63,9 @@
         mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
         final Locale locale = res.getConfiguration().locale;
         // Heuristic: we use American Typography rules because it's the most common rules for all
-        // English variants.
+        // English variants. German rules (not "German typography") also have small gotchas.
         mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage());
+        mUsesGermanRules = Locale.GERMAN.getLanguage().equals(locale.getLanguage());
     }
 
     // Helper functions to create member values.
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 057e332..702688f 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -139,6 +139,20 @@
             j--;
         }
         if (j <= 0 || Character.isWhitespace(prevChar)) {
+            if (spacingAndPunctuations.mUsesGermanRules) {
+                // In German typography rules, there is a specific case that the first character
+                // of a new line should not be capitalized if the previous line ends in a comma.
+                boolean hasNewLine = false;
+                while (--j >= 0 && Character.isWhitespace(prevChar)) {
+                    if (Constants.CODE_ENTER == prevChar) {
+                        hasNewLine = true;
+                    }
+                    prevChar = cs.charAt(j);
+                }
+                if (Constants.CODE_COMMA == prevChar && hasNewLine) {
+                    return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+                }
+            }
             // There are only spacing chars between the start of the paragraph and the cursor,
             // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both
             // MODE_WORDS and MODE_SENTENCES should be active.
diff --git a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
index 40a103b..020d632 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
@@ -94,5 +94,20 @@
         allPathsForCaps("\"Word.\" ", c | w, sp, false);
         allPathsForCaps("\"Word\". ", c | w | s, sp, false);
         allPathsForCaps("\"Word\" ", c | w, sp, false);
+
+        // Test special case for German. German does not capitalize at the start of a
+        // line when the previous line starts with a comma. It does in other cases.
+        sp = job.runInLocale(res, Locale.GERMAN);
+        allPathsForCaps("Liebe Sara,\n", c | w, sp, false);
+        allPathsForCaps("Liebe Sara,\n", c | w, sp, true);
+        allPathsForCaps("Liebe Sara,  \n  ", c | w, sp, false);
+        allPathsForCaps("Liebe Sara  \n  ", c | w | s, sp, false);
+        allPathsForCaps("Liebe Sara.\n  ", c | w | s, sp, false);
+        sp = job.runInLocale(res, Locale.ENGLISH);
+        allPathsForCaps("Liebe Sara,\n", c | w | s, sp, false);
+        allPathsForCaps("Liebe Sara,\n", c | w | s, sp, true);
+        allPathsForCaps("Liebe Sara,  \n  ", c | w | s, sp, false);
+        allPathsForCaps("Liebe Sara  \n  ", c | w | s, sp, false);
+        allPathsForCaps("Liebe Sara.\n  ", c | w | s, sp, false);
     }
 }