Merge "[IL61] Remove an internal use of key coordinates"
diff --git a/java/res/values-fr-rCA/config-spacing-and-punctuations.xml b/java/res/values-fr-rCA/config-spacing-and-punctuations.xml
index 0625480..c3a1a0a 100644
--- a/java/res/values-fr-rCA/config-spacing-and-punctuations.xml
+++ b/java/res/values-fr-rCA/config-spacing-and-punctuations.xml
@@ -25,7 +25,7 @@
     <string name="symbols_followed_by_space" translatable="false">.,;:!?)]}&amp;</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;&#x000A;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors" translatable="false">\'-</string>
 </resources>
diff --git a/java/res/values-fr/config-spacing-and-punctuations.xml b/java/res/values-fr/config-spacing-and-punctuations.xml
index 33e0236..8b53fb3 100644
--- a/java/res/values-fr/config-spacing-and-punctuations.xml
+++ b/java/res/values-fr/config-spacing-and-punctuations.xml
@@ -24,7 +24,7 @@
     <string name="symbols_followed_by_space" translatable="false">.,;:!?)]}&amp;</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;&#x000A;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors" translatable="false">\'-</string>
 </resources>
diff --git a/java/res/values-hy-rAM/config-spacing-and-punctuations.xml b/java/res/values-hy-rAM/config-spacing-and-punctuations.xml
index f26a30d..8bc1b85 100644
--- a/java/res/values-hy-rAM/config-spacing-and-punctuations.xml
+++ b/java/res/values-hy-rAM/config-spacing-and-punctuations.xml
@@ -25,7 +25,7 @@
     <string name="symbols_followed_by_space" translatable="false">.,;:!?)]}&amp;&#x0589;&#x055D;</string>
     <!-- Symbols that separate words. Adding armenian period and comma. -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"&#x0589;&#x055D;</string>
+    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;&#x000A;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"&#x0589;&#x055D;</string>
     <!-- The sentence separator code point, for capitalization -->
     <!-- U+0589: "։" ARMENIAN FULL STOP   ; 589h = 1417d -->
     <integer name="sentence_separator" translatable="false">1417</integer>
diff --git a/java/res/values/config-spacing-and-punctuations.xml b/java/res/values/config-spacing-and-punctuations.xml
index f10f810..2f52edd 100644
--- a/java/res/values/config-spacing-and-punctuations.xml
+++ b/java/res/values/config-spacing-and-punctuations.xml
@@ -28,7 +28,7 @@
     <string name="symbols_followed_by_space" translatable="false">.,;:!?)]}&amp;</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
-    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
+    <string name="symbols_word_separators" translatable="false">"&#x0009;&#x0020;&#x000A;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors" translatable="false">\'-</string>
     <!-- The sentence separator code point, for capitalization -->
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
index 1aee22b..1c6a14e 100644
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -16,12 +16,12 @@
 
 package com.android.inputmethod.latin;
 
-import android.content.Context;
 import android.util.Log;
 
 import com.android.inputmethod.latin.makedict.DictEncoder;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver2DictEncoder;
+import com.android.inputmethod.latin.makedict.Ver4DictEncoder;
+import com.android.inputmethod.latin.utils.FileUtils;
 
 import java.io.File;
 import java.io.IOException;
@@ -31,10 +31,7 @@
     /** Used for Log actions from this class */
     private static final String TAG = AbstractDictionaryWriter.class.getSimpleName();
 
-    private final Context mContext;
-
-    public AbstractDictionaryWriter(final Context context) {
-        mContext = context;
+    public AbstractDictionaryWriter() {
     }
 
     abstract public void clear();
@@ -61,12 +58,11 @@
             final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException;
 
     public void write(final File file, final Map<String, String> attributeMap) {
-        final String tempFilePath = file.getAbsolutePath() + ".temp";
-        final File tempFile = new File(tempFilePath);
         try {
-            final DictEncoder dictEncoder = new Ver2DictEncoder(tempFile);
+            FileUtils.deleteRecursively(file);
+            file.mkdir();
+            final DictEncoder dictEncoder = new Ver4DictEncoder(file);
             writeDictionary(dictEncoder, attributeMap);
-            tempFile.renameTo(file);
         } catch (IOException e) {
             Log.e(TAG, "IO exception while writing file", e);
         } catch (UnsupportedFormatException e) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 2c79986..e66cfca 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -165,6 +165,7 @@
             LanguageModelParam[] languageModelParams, int startIndex);
     private static native int calculateProbabilityNative(long dict, int unigramProbability,
             int bigramProbability);
+    private static native int setCurrentTimeForTestNative(int currentTime);
     private static native String getPropertyNative(long dict, String query);
 
     @UsedForTesting
@@ -420,8 +421,22 @@
         return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability);
     }
 
+    /**
+     * Control the current time to be used in the native code. If currentTime >= 0, this method sets
+     * the current time and gets into test mode.
+     * In test mode, set timestamp is used as the current time in the native code.
+     * If currentTime < 0, quit the test mode and returns to using time() to get the current time.
+     *
+     * @param currentTime seconds since the unix epoch
+     * @return current time got in the native code.
+     */
     @UsedForTesting
-    public String getPropertyForTests(String query) {
+    public static int setCurrentTimeForTest(final int currentTime) {
+        return setCurrentTimeForTestNative(currentTime);
+    }
+
+    @UsedForTesting
+    public String getPropertyForTest(final String query) {
         if (!isValidDictionary()) return "";
         return getPropertyNative(mNativeDict, query);
     }
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/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index 89ef96d..8489868 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import android.content.Context;
-
 import com.android.inputmethod.latin.makedict.DictEncoder;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
@@ -35,14 +33,13 @@
  * An in memory dictionary for memorizing entries and writing a binary dictionary.
  */
 public class DictionaryWriter extends AbstractDictionaryWriter {
-    private static final int BINARY_DICT_VERSION = 2;
+    private static final int BINARY_DICT_VERSION = FormatSpec.VERSION4;
     private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
-            new FormatSpec.FormatOptions(BINARY_DICT_VERSION, false /* supportsDynamicUpdate */);
+            new FormatSpec.FormatOptions(BINARY_DICT_VERSION, false /* hasTimestamp */);
 
     private FusionDictionary mFusionDictionary;
 
-    public DictionaryWriter(final Context context) {
-        super(context);
+    public DictionaryWriter() {
         clear();
     }
 
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index ecef20e..8e1675b 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -136,12 +136,8 @@
      */
     protected abstract boolean hasContentChanged();
 
-    protected boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
-        // This class is using format 2 because it's used by the User and Contacts dictionary
-        // only, which right now use format 2 (dicts using format 4 use Decaying*, which overrides
-        // this method).
-        // TODO: Migrate these dicts to ver4 format, and remove this function.
-        return formatVersion == FormatSpec.VERSION2;
+    private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
+        return formatVersion == FormatSpec.VERSION4;
     }
 
     public boolean isValidDictionary() {
@@ -194,12 +190,12 @@
         }
     }
 
-    private static AbstractDictionaryWriter getDictionaryWriter(final Context context,
+    private static AbstractDictionaryWriter getDictionaryWriter(
             final boolean isDynamicPersonalizationDictionary) {
         if (isDynamicPersonalizationDictionary) {
              return null;
         } else {
-            return new DictionaryWriter(context);
+            return new DictionaryWriter();
         }
     }
 
@@ -233,7 +229,7 @@
         mBinaryDictionary = null;
         mDictNameDictionaryUpdateController = getDictionaryUpdateController(dictName);
         // Currently, only dynamic personalization dictionary is updatable.
-        mDictionaryWriter = getDictionaryWriter(context, isUpdatable);
+        mDictionaryWriter = getDictionaryWriter(isUpdatable);
     }
 
     protected static String getDictNameWithLocale(final String name, final Locale locale) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 3fca4fd..d3e6a1b 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1407,7 +1407,7 @@
 
     // TODO[IL]: Move this to InputLogic
     public SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
-            final SuggestedWords suggestedWords) {
+            final SuggestedWords suggestedWords, final SuggestedWords previousSuggestedWords) {
         // TODO: consolidate this into getSuggestedWords
         // We update the suggestion strip only when we have some suggestions to show, i.e. when
         // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
@@ -1420,30 +1420,24 @@
                 || mSuggestionStripView.isShowingAddToDictionaryHint()) {
             return suggestedWords;
         } else {
-            return getOlderSuggestions(typedWord);
+            final SuggestedWords punctuationList =
+                    mSettings.getCurrent().mSpacingAndPunctuations.mSuggestPuncList;
+            final SuggestedWords oldSuggestedWords = previousSuggestedWords == punctuationList
+                            ? SuggestedWords.EMPTY : previousSuggestedWords;
+            if (TextUtils.isEmpty(typedWord)) {
+                return oldSuggestedWords;
+            }
+            final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
+                    SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
+            return new SuggestedWords(typedWordAndPreviousSuggestions,
+                    false /* typedWordValid */,
+                    false /* hasAutoCorrectionCandidate */,
+                    false /* isPunctuationSuggestions */,
+                    true /* isObsoleteSuggestions */,
+                    false /* isPrediction */);
         }
     }
 
-    private SuggestedWords getOlderSuggestions(final String typedWord) {
-        SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords;
-        if (previousSuggestedWords
-                == mSettings.getCurrent().mSpacingAndPunctuations.mSuggestPuncList) {
-            previousSuggestedWords = SuggestedWords.EMPTY;
-        }
-        if (typedWord == null) {
-            return previousSuggestedWords;
-        }
-        final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
-                SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord,
-                        previousSuggestedWords);
-        return new SuggestedWords(typedWordAndPreviousSuggestions,
-                false /* typedWordValid */,
-                false /* hasAutoCorrectionCandidate */,
-                false /* isPunctuationSuggestions */,
-                true /* isObsoleteSuggestions */,
-                false /* isPrediction */);
-    }
-
     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
             final String typedWord) {
       if (suggestedWords.isEmpty()) {
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 79d6674..325a0d9 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;
@@ -36,6 +35,7 @@
 import com.android.inputmethod.latin.utils.TextRange;
 import com.android.inputmethod.research.ResearchLogger;
 
+import java.util.Arrays;
 import java.util.regex.Pattern;
 
 /**
@@ -98,7 +98,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 +253,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 +271,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 +303,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 +337,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) {
@@ -563,8 +558,8 @@
         return getNthPreviousWord(prev, spacingAndPunctuations, n);
     }
 
-    private static boolean isSeparator(int code, String sep) {
-        return sep.indexOf(code) != -1;
+    private static boolean isSeparator(final int code, final int[] sortedSeparators) {
+        return Arrays.binarySearch(sortedSeparators, code) >= 0;
     }
 
     // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
@@ -603,29 +598,29 @@
     }
 
     /**
-     * @param separators characters which may separate words
+     * @param sortedSeparators a sorted array of code points which may separate words
      * @return the word that surrounds the cursor, including up to one trailing
      *   separator. For example, if the field contains "he|llo world", where |
      *   represents the cursor, then "hello " will be returned.
      */
-    public CharSequence getWordAtCursor(String separators) {
+    public CharSequence getWordAtCursor(final int[] sortedSeparators) {
         // getWordRangeAtCursor returns null if the connection is null
-        TextRange r = getWordRangeAtCursor(separators, 0);
+        final TextRange r = getWordRangeAtCursor(sortedSeparators, 0);
         return (r == null) ? null : r.mWord;
     }
 
     /**
      * Returns the text surrounding the cursor.
      *
-     * @param sep a string of characters that split words.
+     * @param sortedSeparators a sorted array of code points that split words.
      * @param additionalPrecedingWordsCount the number of words before the current word that should
      *   be included in the returned range
      * @return a range containing the text surrounding the cursor
      */
-    public TextRange getWordRangeAtCursor(final String sep,
+    public TextRange getWordRangeAtCursor(final int[] sortedSeparators,
             final int additionalPrecedingWordsCount) {
         mIC = mParent.getCurrentInputConnection();
-        if (mIC == null || sep == null) {
+        if (mIC == null) {
             return null;
         }
         final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
@@ -644,7 +639,7 @@
         while (true) { // see comments below for why this is guaranteed to halt
             while (startIndexInBefore > 0) {
                 final int codePoint = Character.codePointBefore(before, startIndexInBefore);
-                if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+                if (isStoppingAtWhitespace == isSeparator(codePoint, sortedSeparators)) {
                     break;  // inner loop
                 }
                 --startIndexInBefore;
@@ -665,7 +660,7 @@
         int endIndexInAfter = -1;
         while (++endIndexInAfter < after.length()) {
             final int codePoint = Character.codePointAt(after, endIndexInAfter);
-            if (isSeparator(codePoint, sep)) {
+            if (isSeparator(codePoint, sortedSeparators)) {
                 break;
             }
             if (Character.isSupplementaryCodePoint(codePoint)) {
@@ -681,23 +676,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 0c33e6b..c040ee8 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -534,7 +534,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);
             }
@@ -969,7 +970,8 @@
             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
             mRecapitalizeStatus.initialize(mConnection.getExpectedSelectionStart(),
                     mConnection.getExpectedSelectionEnd(), selectedText.toString(),
-                    settingsValues.mLocale, settingsValues.mSpacingAndPunctuations.mWordSeparators);
+                    settingsValues.mLocale,
+                    settingsValues.mSpacingAndPunctuations.mSortedWordSeparators);
             // We trim leading and trailing whitespace.
             mRecapitalizeStatus.trim();
         }
@@ -1030,7 +1032,8 @@
                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
                         final SuggestedWords suggestedWordsWithMaybeOlderSuggestions =
                                 mLatinIME.maybeRetrieveOlderSuggestions(
-                                        mWordComposer.getTypedWord(), suggestedWords);
+                                        mWordComposer.getTypedWord(), suggestedWords,
+                                        mSuggestedWords);
                         holder.set(suggestedWordsWithMaybeOlderSuggestions);
                     }
                 }
@@ -1069,9 +1072,9 @@
         // 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,
+                settingsValues.mSpacingAndPunctuations.mSortedWordSeparators,
                 0 /* additionalPrecedingWordsCount */);
         if (null == range) return; // Happens if we don't have an input connection at all
         if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
@@ -1290,7 +1293,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);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
index e543042..a3a6c2c 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
@@ -33,6 +34,7 @@
 /**
  * An implementation of DictEncoder for version 2 binary dictionary.
  */
+@UsedForTesting
 public class Ver2DictEncoder implements DictEncoder {
 
     private final File mDictFile;
@@ -40,6 +42,7 @@
     private byte[] mBuffer;
     private int mPosition;
 
+    @UsedForTesting
     public Ver2DictEncoder(final File dictFile) {
         mDictFile = dictFile;
         mOutStream = null;
@@ -49,6 +52,7 @@
     // This constructor is used only by BinaryDictOffdeviceUtilsTests.
     // If you want to use this in the production code, you should consider keeping consistency of
     // the interface of Ver3DictDecoder by using factory.
+    @UsedForTesting
     public Ver2DictEncoder(final OutputStream outStream) {
         mDictFile = null;
         mOutStream = outStream;
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index cc57d13..4a610e6 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -116,13 +116,6 @@
         return false;
     }
 
-    @Override
-    protected boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
-        // This class is using format 4 because it's used by all version 4 dictionaries.
-        // TODO: remove this when all dynamically generated dicts use version 4.
-        return formatVersion == REQUIRED_BINARY_DICTIONARY_VERSION;
-    }
-
     public void addMultipleDictionaryEntriesToDictionary(
             final ArrayList<LanguageModelParam> languageModelParams,
             final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 7db1071..9bf269b 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -181,10 +181,6 @@
         return mSettingsValues.mIsInternal;
     }
 
-    public String getWordSeparators() {
-        return mSettingsValues.mSpacingAndPunctuations.mWordSeparators;
-    }
-
     public boolean isWordSeparator(final int code) {
         return mSettingsValues.isWordSeparator(code);
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index dbe30e2..8ba32ff 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -32,38 +32,41 @@
 import java.util.Locale;
 
 public final class SpacingAndPunctuations {
-    private final int[] mSymbolsPrecededBySpace;
-    private final int[] mSymbolsFollowedBySpace;
-    private final int[] mWordConnectors;
+    private final int[] mSortedSymbolsPrecededBySpace;
+    private final int[] mSortedSymbolsFollowedBySpace;
+    private final int[] mSortedWordConnectors;
+    public final int[] mSortedWordSeparators;
     public final SuggestedWords mSuggestPuncList;
-    public final String mWordSeparators;
     private final int mSentenceSeparator;
     public final String mSentenceSeparatorAndSpace;
     public final boolean mCurrentLanguageHasSpaces;
     public final boolean mUsesAmericanTypography;
+    public final boolean mUsesGermanRules;
 
     public SpacingAndPunctuations(final Resources res) {
-        mSymbolsPrecededBySpace =
-                StringUtils.toCodePointArray(res.getString(R.string.symbols_preceded_by_space));
-        Arrays.sort(mSymbolsPrecededBySpace);
-        mSymbolsFollowedBySpace =
-                StringUtils.toCodePointArray(res.getString(R.string.symbols_followed_by_space));
-        Arrays.sort(mSymbolsFollowedBySpace);
-        mWordConnectors =
-                StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors));
-        Arrays.sort(mWordConnectors);
+        // To be able to binary search the code point. See {@link #isUsuallyPrecededBySpace(int)}.
+        mSortedSymbolsPrecededBySpace = StringUtils.toSortedCodePointArray(
+                res.getString(R.string.symbols_preceded_by_space));
+        // To be able to binary search the code point. See {@link #isUsuallyFollowedBySpace(int)}.
+        mSortedSymbolsFollowedBySpace = StringUtils.toSortedCodePointArray(
+                res.getString(R.string.symbols_followed_by_space));
+        // To be able to binary search the code point. See {@link #isWordConnector(int)}.
+        mSortedWordConnectors = StringUtils.toSortedCodePointArray(
+                res.getString(R.string.symbols_word_connectors));
+        mSortedWordSeparators = StringUtils.toSortedCodePointArray(
+                res.getString(R.string.symbols_word_separators));
         final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
-        mWordSeparators = res.getString(R.string.symbols_word_separators);
         mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
         mSentenceSeparatorAndSpace = new String(new int[] {
                 mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
         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.
@@ -72,6 +75,7 @@
         if (puncs != null) {
             for (final String puncSpec : puncs) {
                 // TODO: Stop using KeySpceParser.getLabel().
+                // TODO: Punctuation suggestions should honor RTL languages.
                 puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
                         SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
                         Dictionary.DICTIONARY_HARDCODED,
@@ -88,11 +92,11 @@
     }
 
     public boolean isWordSeparator(final int code) {
-        return mWordSeparators.contains(String.valueOf((char)code));
+        return Arrays.binarySearch(mSortedWordSeparators, code) >= 0;
     }
 
     public boolean isWordConnector(final int code) {
-        return Arrays.binarySearch(mWordConnectors, code) >= 0;
+        return Arrays.binarySearch(mSortedWordConnectors, code) >= 0;
     }
 
     public boolean isWordCodePoint(final int code) {
@@ -100,11 +104,11 @@
     }
 
     public boolean isUsuallyPrecededBySpace(final int code) {
-        return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0;
+        return Arrays.binarySearch(mSortedSymbolsPrecededBySpace, code) >= 0;
     }
 
     public boolean isUsuallyFollowedBySpace(final int code) {
-        return Arrays.binarySearch(mSymbolsFollowedBySpace, code) >= 0;
+        return Arrays.binarySearch(mSortedSymbolsFollowedBySpace, code) >= 0;
     }
 
     public boolean isSentenceSeparator(final int code) {
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/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
index 0f5cd80..4521ec5 100644
--- a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
+++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
@@ -37,12 +37,12 @@
         CAPS_MODE_ALL_UPPER
     };
 
-    private static final int getStringMode(final String string, final String separators) {
+    private static final int getStringMode(final String string, final int[] sortedSeparators) {
         if (StringUtils.isIdenticalAfterUpcase(string)) {
             return CAPS_MODE_ALL_UPPER;
         } else if (StringUtils.isIdenticalAfterDowncase(string)) {
             return CAPS_MODE_ALL_LOWER;
-        } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) {
+        } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, sortedSeparators)) {
             return CAPS_MODE_FIRST_WORD_UPPER;
         } else {
             return CAPS_MODE_ORIGINAL_MIXED_CASE;
@@ -60,26 +60,28 @@
     private int mRotationStyleCurrentIndex;
     private boolean mSkipOriginalMixedCaseMode;
     private Locale mLocale;
-    private String mSeparators;
+    private int[] mSortedSeparators;
     private String mStringAfter;
     private boolean mIsActive;
 
+    private static final int[] EMPTY_STORTED_SEPARATORS = {};
+
     public RecapitalizeStatus() {
         // By default, initialize with dummy values that won't match any real recapitalize.
-        initialize(-1, -1, "", Locale.getDefault(), "");
+        initialize(-1, -1, "", Locale.getDefault(), EMPTY_STORTED_SEPARATORS);
         deactivate();
     }
 
     public void initialize(final int cursorStart, final int cursorEnd, final String string,
-            final Locale locale, final String separators) {
+            final Locale locale, final int[] sortedSeparators) {
         mCursorStartBefore = cursorStart;
         mStringBefore = string;
         mCursorStartAfter = cursorStart;
         mCursorEndAfter = cursorEnd;
         mStringAfter = string;
-        final int initialMode = getStringMode(mStringBefore, separators);
+        final int initialMode = getStringMode(mStringBefore, sortedSeparators);
         mLocale = locale;
-        mSeparators = separators;
+        mSortedSeparators = sortedSeparators;
         if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) {
             mRotationStyleCurrentIndex = 0;
             mSkipOriginalMixedCaseMode = false;
@@ -131,7 +133,7 @@
                 mStringAfter = mStringBefore.toLowerCase(mLocale);
                 break;
             case CAPS_MODE_FIRST_WORD_UPPER:
-                mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators,
+                mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSortedSeparators,
                         mLocale);
                 break;
             case CAPS_MODE_ALL_UPPER:
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index c5ed393..5920c68 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.Constants;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Locale;
 
 public final class StringUtils {
@@ -183,6 +184,12 @@
         return codePoints;
     }
 
+    public static int[] toSortedCodePointArray(final String string) {
+        final int[] codePoints = toCodePointArray(string);
+        Arrays.sort(codePoints);
+        return codePoints;
+    }
+
     // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
     public static int getCapitalizationType(final String text) {
         // If the first char is not uppercase, then the word is either all lower case or
@@ -265,39 +272,39 @@
     }
 
     public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
-            final String separators) {
-        boolean needCapsNext = true;
+            final int[] sortedSeparators) {
+        boolean needsCapsNext = true;
         final int len = text.length();
         for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
             final int codePoint = text.codePointAt(i);
             if (Character.isLetter(codePoint)) {
-                if ((needCapsNext && !Character.isUpperCase(codePoint))
-                        || (!needCapsNext && !Character.isLowerCase(codePoint))) {
+                if ((needsCapsNext && !Character.isUpperCase(codePoint))
+                        || (!needsCapsNext && !Character.isLowerCase(codePoint))) {
                     return false;
                 }
             }
             // We need a capital letter next if this is a separator.
-            needCapsNext = (-1 != separators.indexOf(codePoint));
+            needsCapsNext = (Arrays.binarySearch(sortedSeparators, codePoint) >= 0);
         }
         return true;
     }
 
     // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph
     // which should be capitalized together in *some* cases.
-    public static String capitalizeEachWord(final String text, final String separators,
+    public static String capitalizeEachWord(final String text, final int[] sortedSeparators,
             final Locale locale) {
         final StringBuilder builder = new StringBuilder();
-        boolean needCapsNext = true;
+        boolean needsCapsNext = true;
         final int len = text.length();
         for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
             final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1));
-            if (needCapsNext) {
+            if (needsCapsNext) {
                 builder.append(nextChar.toUpperCase(locale));
             } else {
                 builder.append(nextChar.toLowerCase(locale));
             }
             // We need a capital letter next if this is a separator.
-            needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0)));
+            needsCapsNext = (Arrays.binarySearch(sortedSeparators, nextChar.codePointAt(0)) >= 0);
         }
         return builder.toString();
     }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index e7f49a6..11fb3a1 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -59,6 +59,7 @@
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
 import com.android.inputmethod.latin.utils.TextRange;
 import com.android.inputmethod.research.MotionEventReader.ReplayData;
 import com.android.inputmethod.research.ui.SplashScreen;
@@ -131,7 +132,8 @@
     public static final String RESEARCH_KEY_OUTPUT_TEXT = ".research.";
 
     // constants related to specific log points
-    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
+    private static final int[] WHITESPACE_SEPARATORS =
+            StringUtils.toSortedCodePointArray(" \t\n\r");
     private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
     private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
 
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 26570f4..586a306 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -29,6 +29,7 @@
 #include "suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.h"
 #include "suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h"
 #include "utils/autocorrection_threshold_utils.h"
+#include "utils/time_keeper.h"
 
 namespace latinime {
 
@@ -457,6 +458,17 @@
     return env->NewStringUTF(resultChars);
 }
 
+static int latinime_BinaryDictionary_setCurrentTimeForTest(JNIEnv *env, jclass clazz,
+        jint currentTime) {
+    if (currentTime >= 0) {
+        TimeKeeper::startTestModeWithForceCurrentTime(currentTime);
+    } else {
+        TimeKeeper::stopTestMode();
+    }
+    TimeKeeper::setCurrentTime();
+    return TimeKeeper::peekCurrentTime();
+}
+
 static const JNINativeMethod sMethods[] = {
     {
         const_cast<char *>("createEmptyDictFileNative"),
@@ -550,6 +562,11 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_calculateProbabilityNative)
     },
     {
+        const_cast<char *>("setCurrentTimeForTestNative"),
+        const_cast<char *>("(I)I"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_setCurrentTimeForTest)
+    },
+    {
         const_cast<char *>("getPropertyNative"),
         const_cast<char *>("(JLjava/lang/String;)Ljava/lang/String;"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_getProperty)
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
index 5540b6d..71bcab6 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
+++ b/native/jni/src/suggest/core/dicnode/dic_node_utils.cpp
@@ -118,7 +118,7 @@
         actualLength0 = i + 1;
     }
     actualLength0 = min(actualLength0, MAX_WORD_LENGTH);
-    memcpy(dest, src0, actualLength0 * sizeof(dest[0]));
+    memmove(dest, src0, actualLength0 * sizeof(dest[0]));
     if (!src1 || length1 == 0) {
         return actualLength0;
     }
@@ -130,7 +130,7 @@
         actualLength1 = i + 1;
     }
     actualLength1 = min(actualLength1, MAX_WORD_LENGTH - actualLength0);
-    memcpy(&dest[actualLength0], src1, actualLength1 * sizeof(dest[0]));
+    memmove(&dest[actualLength0], src1, actualLength1 * sizeof(dest[0]));
     return actualLength0 + actualLength1;
 }
 } // namespace latinime
diff --git a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_output.h b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_output.h
index 74eb5df..fc68510 100644
--- a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_output.h
+++ b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_output.h
@@ -17,7 +17,7 @@
 #ifndef LATINIME_DIC_NODE_STATE_OUTPUT_H
 #define LATINIME_DIC_NODE_STATE_OUTPUT_H
 
-#include <cstring> // for memcpy()
+#include <cstring> // for memmove()
 #include <stdint.h>
 
 #include "defines.h"
@@ -38,7 +38,7 @@
     }
 
     void init(const DicNodeStateOutput *const stateOutput) {
-        memcpy(mCodePointsBuf, stateOutput->mCodePointsBuf,
+        memmove(mCodePointsBuf, stateOutput->mCodePointsBuf,
                 stateOutput->mOutputtedCodePointCount * sizeof(mCodePointsBuf[0]));
         mOutputtedCodePointCount = stateOutput->mOutputtedCodePointCount;
         if (mOutputtedCodePointCount < MAX_WORD_LENGTH) {
@@ -51,7 +51,7 @@
         if (mergedNodeCodePoints) {
             const int additionalCodePointCount = min(static_cast<int>(mergedNodeCodePointCount),
                     MAX_WORD_LENGTH - mOutputtedCodePointCount);
-            memcpy(&mCodePointsBuf[mOutputtedCodePointCount], mergedNodeCodePoints,
+            memmove(&mCodePointsBuf[mOutputtedCodePointCount], mergedNodeCodePoints,
                     additionalCodePointCount * sizeof(mCodePointsBuf[0]));
             mOutputtedCodePointCount = static_cast<uint16_t>(
                     mOutputtedCodePointCount + mergedNodeCodePointCount);
diff --git a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
index dba5705..e7108d9 100644
--- a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
+++ b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_prevword.h
@@ -17,7 +17,7 @@
 #ifndef LATINIME_DIC_NODE_STATE_PREVWORD_H
 #define LATINIME_DIC_NODE_STATE_PREVWORD_H
 
-#include <cstring> // for memset()
+#include <cstring> // for memset() and memmove()
 #include <stdint.h>
 
 #include "defines.h"
@@ -62,7 +62,7 @@
         mPrevWordProbability = prevWord->mPrevWordProbability;
         mPrevWordPtNodePos = prevWord->mPrevWordPtNodePos;
         mSecondWordFirstInputIndex = prevWord->mSecondWordFirstInputIndex;
-        memcpy(mPrevWord, prevWord->mPrevWord, prevWord->mPrevWordLength * sizeof(mPrevWord[0]));
+        memmove(mPrevWord, prevWord->mPrevWord, prevWord->mPrevWordLength * sizeof(mPrevWord[0]));
     }
 
     void init(const int16_t prevWordCount, const int16_t prevWordProbability,
diff --git a/native/jni/src/suggest/core/layout/proximity_info_state.cpp b/native/jni/src/suggest/core/layout/proximity_info_state.cpp
index de99e2f..40c3448 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_state.cpp
+++ b/native/jni/src/suggest/core/layout/proximity_info_state.cpp
@@ -18,7 +18,7 @@
 
 #include "suggest/core/layout/proximity_info_state.h"
 
-#include <cstring> // for memset() and memcpy()
+#include <cstring> // for memset() and memmove()
 #include <sstream> // for debug prints
 #include <vector>
 
@@ -285,7 +285,7 @@
 }
 
 float ProximityInfoState::getMostProbableString(int *const codePointBuf) const {
-    memcpy(codePointBuf, mMostProbableString, sizeof(mMostProbableString));
+    memmove(codePointBuf, mMostProbableString, sizeof(mMostProbableString));
     return mMostProbableStringProbability;
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index 96bb812..448962e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -34,11 +34,6 @@
 const char *const Ver4PatriciaTriePolicy::BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
 const char *const Ver4PatriciaTriePolicy::MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
 const char *const Ver4PatriciaTriePolicy::MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
-const char *const Ver4PatriciaTriePolicy::SET_CURRENT_TIME_FOR_TESTING_QUERY_FORMAT =
-        "SET_CURRENT_TIME_FOR_TESTING:%d";
-const char *const Ver4PatriciaTriePolicy::GET_CURRENT_TIME_QUERY = "GET_CURRENT_TIME";
-const char *const Ver4PatriciaTriePolicy::QUIT_TIMEKEEPER_TEST_MODE_QUERY =
-        "QUIT_TIMEKEEPER_TEST_MODE";
 const int Ver4PatriciaTriePolicy::MARGIN_TO_REFUSE_DYNAMIC_OPERATIONS = 1024;
 const int Ver4PatriciaTriePolicy::MIN_DICT_SIZE_TO_REFUSE_DYNAMIC_OPERATIONS =
         Ver4DictConstants::MAX_DICTIONARY_SIZE - MARGIN_TO_REFUSE_DYNAMIC_OPERATIONS;
@@ -288,7 +283,6 @@
 void Ver4PatriciaTriePolicy::getProperty(const char *const query, const int queryLength,
         char *const outResult, const int maxResultLength) {
     const int compareLength = queryLength + 1 /* terminator */;
-    int timestamp = NOT_A_TIMESTAMP;
     if (strncmp(query, UNIGRAM_COUNT_QUERY, compareLength) == 0) {
         snprintf(outResult, maxResultLength, "%d", mUnigramCount);
     } else if (strncmp(query, BIGRAM_COUNT_QUERY, compareLength) == 0) {
@@ -301,12 +295,6 @@
         snprintf(outResult, maxResultLength, "%d",
                 mHeaderPolicy->isDecayingDict() ? ForgettingCurveUtils::MAX_BIGRAM_COUNT :
                         static_cast<int>(Ver4DictConstants::MAX_DICTIONARY_SIZE));
-    } else if (sscanf(query, SET_CURRENT_TIME_FOR_TESTING_QUERY_FORMAT, &timestamp) == 1) {
-        TimeKeeper::startTestModeWithForceCurrentTime(timestamp);
-    } else if (strncmp(query, GET_CURRENT_TIME_QUERY, compareLength) == 0) {
-        snprintf(outResult, maxResultLength, "%d", TimeKeeper::peekCurrentTime());
-    } else if (strncmp(query, QUIT_TIMEKEEPER_TEST_MODE_QUERY, compareLength) == 0) {
-        TimeKeeper::stopTestMode();
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
index 8187b7a..81aed20 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
@@ -116,9 +116,6 @@
     static const char *const BIGRAM_COUNT_QUERY;
     static const char *const MAX_UNIGRAM_COUNT_QUERY;
     static const char *const MAX_BIGRAM_COUNT_QUERY;
-    static const char *const SET_CURRENT_TIME_FOR_TESTING_QUERY_FORMAT;
-    static const char *const GET_CURRENT_TIME_QUERY;
-    static const char *const QUIT_TIMEKEEPER_TEST_MODE_QUERY;
     // When the dictionary size is near the maximum size, we have to refuse dynamic operations to
     // prevent the dictionary from overflowing.
     static const int MARGIN_TO_REFUSE_DYNAMIC_OPERATIONS;
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index 098b211..1dc1f5a 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -37,14 +37,6 @@
 public class BinaryDictionaryDecayingTests extends AndroidTestCase {
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
     private static final String TEST_LOCALE = "test";
-
-    // Note that these are corresponding definitions in native code in
-    // latinime::Ver4PatriciaTriePolicy.
-    private static final String SET_CURRENT_TIME_FOR_TESTING_QUERY =
-            "SET_CURRENT_TIME_FOR_TESTING";
-    private static final String GET_CURRENT_TIME_QUERY = "GET_CURRENT_TIME";
-    private static final String QUIT_TIMEKEEPER_TEST_MODE_QUERY = "QUIT_TIMEKEEPER_TEST_MODE";
-
     private static final int DUMMY_PROBABILITY = 0;
 
     private int mCurrentTime = 0;
@@ -58,17 +50,7 @@
     @Override
     protected void tearDown() throws Exception {
         super.tearDown();
-        try {
-            final File dictFile =
-                    createEmptyDictionaryAndGetFile("TestBinaryDictionary", FormatSpec.VERSION4);
-            final BinaryDictionary binaryDictionary =
-                    new BinaryDictionary(dictFile.getAbsolutePath(), 0 /* offset */,
-                            dictFile.length(), true /* useFullEditDistance */, Locale.getDefault(),
-                            TEST_LOCALE, true /* isUpdatable */);
-            binaryDictionary.getPropertyForTests(QUIT_TIMEKEEPER_TEST_MODE_QUERY);
-        } catch (IOException e) {
-            fail("IOException while writing an initial dictionary : " + e);
-        }
+        stopTestModeInNativeCode();
     }
 
     private void addUnigramWord(final BinaryDictionary binaryDictionary, final String word,
@@ -89,7 +71,7 @@
         // 4 days.
         final int timeToElapse = (int)TimeUnit.SECONDS.convert(4, TimeUnit.DAYS);
         mCurrentTime += timeToElapse;
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
         binaryDictionary.flushWithGC();
     }
 
@@ -97,7 +79,7 @@
         // 60 days.
         final int timeToElapse = (int)TimeUnit.SECONDS.convert(60, TimeUnit.DAYS);
         mCurrentTime += timeToElapse;
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
         binaryDictionary.flushWithGC();
     }
 
@@ -110,6 +92,7 @@
                     + " is not supported.");
         }
     }
+
     private File createEmptyVer4DictionaryAndGetFile(final String dictId) throws IOException {
         final File file = File.createTempFile(dictId, TEST_DICT_FILE_EXTENSION,
                 getContext().getCacheDir());
@@ -128,14 +111,12 @@
         }
     }
 
-    private static int getCurrentTime(final BinaryDictionary binaryDictionary) {
-        return Integer.parseInt(binaryDictionary.getPropertyForTests(GET_CURRENT_TIME_QUERY));
+    private static int setCurrentTimeForTestMode(final int currentTime) {
+        return BinaryDictionary.setCurrentTimeForTest(currentTime);
     }
 
-    private static void setCurrentTime(final BinaryDictionary binaryDictionary,
-            final int currentTime) {
-        final String query = SET_CURRENT_TIME_FOR_TESTING_QUERY + ":" + currentTime;
-        binaryDictionary.getPropertyForTests(query);
+    private static int stopTestModeInNativeCode() {
+        return BinaryDictionary.setCurrentTimeForTest(-1);
     }
 
     public void testControlCurrentTime() {
@@ -146,24 +127,13 @@
         final int TEST_COUNT = 1000;
         final long seed = System.currentTimeMillis();
         final Random random = new Random(seed);
-        File dictFile = null;
-        try {
-            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary", formatVersion);
-        } catch (IOException e) {
-            fail("IOException while writing an initial dictionary : " + e);
-        }
-        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
-                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
-                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
-        binaryDictionary.getPropertyForTests(QUIT_TIMEKEEPER_TEST_MODE_QUERY);
-        final int startTime = getCurrentTime(binaryDictionary);
+        final int startTime = stopTestModeInNativeCode();
         for (int i = 0; i < TEST_COUNT; i++) {
             final int currentTime = random.nextInt(Integer.MAX_VALUE);
-            setCurrentTime(binaryDictionary, currentTime);
-            assertEquals(currentTime, getCurrentTime(binaryDictionary));
+            final int currentTimeInNativeCode = setCurrentTimeForTestMode(currentTime);
+            assertEquals(currentTime, currentTimeInNativeCode);
         }
-        binaryDictionary.getPropertyForTests(QUIT_TIMEKEEPER_TEST_MODE_QUERY);
-        final int endTime = getCurrentTime(binaryDictionary);
+        final int endTime = stopTestModeInNativeCode();
         final int MAX_ALLOWED_ELAPSED_TIME = 10;
         assertTrue(startTime <= endTime && endTime <= startTime + MAX_ALLOWED_ELAPSED_TIME);
     }
@@ -301,7 +271,7 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
 
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
         final ArrayList<String> words = new ArrayList<String>();
@@ -312,31 +282,31 @@
         }
 
         final int maxUnigramCount = Integer.parseInt(
-                binaryDictionary.getPropertyForTests(BinaryDictionary.MAX_UNIGRAM_COUNT_QUERY));
+                binaryDictionary.getPropertyForTest(BinaryDictionary.MAX_UNIGRAM_COUNT_QUERY));
         for (int i = 0; i < unigramTypedCount; i++) {
             final String word = words.get(random.nextInt(words.size()));
             addUnigramWord(binaryDictionary, word, DUMMY_PROBABILITY);
 
             if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                 final int unigramCountBeforeGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.UNIGRAM_COUNT_QUERY));
                 while (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                     forcePassingShortTime(binaryDictionary);
                 }
                 final int unigramCountAfterGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.UNIGRAM_COUNT_QUERY));
                 assertTrue(unigramCountBeforeGC > unigramCountAfterGC);
             }
         }
 
-        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.UNIGRAM_COUNT_QUERY)) > 0);
-        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.UNIGRAM_COUNT_QUERY)) <= maxUnigramCount);
         forcePassingLongTime(binaryDictionary);
-        assertEquals(0, Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertEquals(0, Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.UNIGRAM_COUNT_QUERY)));
     }
 
@@ -362,7 +332,7 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
 
         final String strong = "strong";
@@ -383,13 +353,13 @@
             }
             if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                 final int unigramCountBeforeGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.UNIGRAM_COUNT_QUERY));
                 assertTrue(binaryDictionary.isValidWord(strong));
                 assertTrue(binaryDictionary.isValidWord(weak));
                 binaryDictionary.flushWithGC();
                 final int unigramCountAfterGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.UNIGRAM_COUNT_QUERY));
                 assertTrue(unigramCountBeforeGC > unigramCountAfterGC);
                 assertFalse(binaryDictionary.isValidWord(weak));
@@ -420,7 +390,7 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
 
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
         final ArrayList<String> words = new ArrayList<String>();
@@ -443,7 +413,7 @@
         }
 
         final int maxBigramCount = Integer.parseInt(
-                binaryDictionary.getPropertyForTests(BinaryDictionary.MAX_BIGRAM_COUNT_QUERY));
+                binaryDictionary.getPropertyForTest(BinaryDictionary.MAX_BIGRAM_COUNT_QUERY));
         for (int i = 0; i < bigramTypedCount; ++i) {
             final Pair<String, String> bigram = bigrams.get(random.nextInt(bigrams.size()));
             addUnigramWord(binaryDictionary, bigram.first, DUMMY_PROBABILITY);
@@ -452,24 +422,24 @@
 
             if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                 final int bigramCountBeforeGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.BIGRAM_COUNT_QUERY));
                 while (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                     forcePassingShortTime(binaryDictionary);
                 }
                 final int bigramCountAfterGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.BIGRAM_COUNT_QUERY));
                 assertTrue(bigramCountBeforeGC > bigramCountAfterGC);
             }
         }
 
-        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.BIGRAM_COUNT_QUERY)) > 0);
-        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.BIGRAM_COUNT_QUERY)) <= maxBigramCount);
         forcePassingLongTime(binaryDictionary);
-        assertEquals(0, Integer.parseInt(binaryDictionary.getPropertyForTests(
+        assertEquals(0, Integer.parseInt(binaryDictionary.getPropertyForTest(
                 BinaryDictionary.BIGRAM_COUNT_QUERY)));
     }
 
@@ -497,7 +467,7 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
-        setCurrentTime(binaryDictionary, mCurrentTime);
+        setCurrentTimeForTestMode(mCurrentTime);
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
 
         final ArrayList<String> words = new ArrayList<String>();
@@ -538,11 +508,11 @@
             }
             if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
                 final int bigramCountBeforeGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.BIGRAM_COUNT_QUERY));
                 binaryDictionary.flushWithGC();
                 final int bigramCountAfterGC =
-                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                        Integer.parseInt(binaryDictionary.getPropertyForTest(
                                 BinaryDictionary.BIGRAM_COUNT_QUERY));
                 assertTrue(bigramCountBeforeGC > bigramCountAfterGC);
                 assertTrue(binaryDictionary.isValidBigram(strong, target));
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index d0059e3..604434f 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -750,14 +750,14 @@
                 addBigramWords(binaryDictionary, word0, word1, bigramProbability);
             }
             assertEquals(new HashSet<String>(words).size(), Integer.parseInt(
-                    binaryDictionary.getPropertyForTests(BinaryDictionary.UNIGRAM_COUNT_QUERY)));
+                    binaryDictionary.getPropertyForTest(BinaryDictionary.UNIGRAM_COUNT_QUERY)));
             assertEquals(new HashSet<Pair<String, String>>(bigrams).size(), Integer.parseInt(
-                    binaryDictionary.getPropertyForTests(BinaryDictionary.BIGRAM_COUNT_QUERY)));
+                    binaryDictionary.getPropertyForTest(BinaryDictionary.BIGRAM_COUNT_QUERY)));
             binaryDictionary.flushWithGC();
             assertEquals(new HashSet<String>(words).size(), Integer.parseInt(
-                    binaryDictionary.getPropertyForTests(BinaryDictionary.UNIGRAM_COUNT_QUERY)));
+                    binaryDictionary.getPropertyForTest(BinaryDictionary.UNIGRAM_COUNT_QUERY)));
             assertEquals(new HashSet<Pair<String, String>>(bigrams).size(), Integer.parseInt(
-                    binaryDictionary.getPropertyForTests(BinaryDictionary.BIGRAM_COUNT_QUERY)));
+                    binaryDictionary.getPropertyForTest(BinaryDictionary.BIGRAM_COUNT_QUERY)));
             binaryDictionary.close();
         }
 
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
index f19d185..7f07435 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
@@ -32,6 +32,7 @@
 
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.utils.RunInLocale;
+import com.android.inputmethod.latin.utils.StringUtils;
 import com.android.inputmethod.latin.utils.TextRange;
 
 import java.util.Locale;
@@ -183,6 +184,12 @@
     /**
      * Test logic in getting the word range at the cursor.
      */
+    private static final int[] SPACE = { Constants.CODE_SPACE };
+    static final int[] TAB = { Constants.CODE_TAB };
+    private static final int[] SPACE_TAB = StringUtils.toSortedCodePointArray(" \t");
+    // A character that needs surrogate pair to represent its code point (U+2008A).
+    private static final String SUPPLEMENTARY_CHAR = "\uD840\uDC8A";
+
     public void testGetWordRangeAtCursor() {
         ExtractedText et = new ExtractedText();
         final MockInputMethodService mockInputMethodService = new MockInputMethodService();
@@ -194,48 +201,47 @@
 
         ic.beginBatchEdit();
         // basic case
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         assertTrue(TextUtils.equals("word", r.mWord));
 
         // more than one word
-        r = ic.getWordRangeAtCursor(" ", 1);
+        r = ic.getWordRangeAtCursor(SPACE, 1);
         assertTrue(TextUtils.equals("word word", r.mWord));
         ic.endBatchEdit();
 
         // tab character instead of space
         mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
         ic.beginBatchEdit();
-        r = ic.getWordRangeAtCursor("\t", 1);
+        r = ic.getWordRangeAtCursor(TAB, 1);
         ic.endBatchEdit();
         assertTrue(TextUtils.equals("word\tword", r.mWord));
 
         // only one word doesn't go too far
         mockInputMethodService.setInputConnection(new MockConnection("one\tword\two", "rd", et));
         ic.beginBatchEdit();
-        r = ic.getWordRangeAtCursor("\t", 1);
+        r = ic.getWordRangeAtCursor(TAB, 1);
         ic.endBatchEdit();
         assertTrue(TextUtils.equals("word\tword", r.mWord));
 
         // tab or space
         mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
         ic.beginBatchEdit();
-        r = ic.getWordRangeAtCursor(" \t", 1);
+        r = ic.getWordRangeAtCursor(SPACE_TAB, 1);
         ic.endBatchEdit();
         assertTrue(TextUtils.equals("word\tword", r.mWord));
 
         // tab or space multiword
         mockInputMethodService.setInputConnection(new MockConnection("one word\two", "rd", et));
         ic.beginBatchEdit();
-        r = ic.getWordRangeAtCursor(" \t", 2);
+        r = ic.getWordRangeAtCursor(SPACE_TAB, 2);
         ic.endBatchEdit();
         assertTrue(TextUtils.equals("one word\tword", r.mWord));
 
         // splitting on supplementary character
-        final String supplementaryChar = "\uD840\uDC8A";
         mockInputMethodService.setInputConnection(
-                new MockConnection("one word" + supplementaryChar + "wo", "rd", et));
+                new MockConnection("one word" + SUPPLEMENTARY_CHAR + "wo", "rd", et));
         ic.beginBatchEdit();
-        r = ic.getWordRangeAtCursor(supplementaryChar, 0);
+        r = ic.getWordRangeAtCursor(StringUtils.toSortedCodePointArray(SUPPLEMENTARY_CHAR), 0);
         ic.endBatchEdit();
         assertTrue(TextUtils.equals("word", r.mWord));
     }
@@ -265,7 +271,7 @@
         TextRange r;
         SuggestionSpan[] suggestions;
 
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 1);
         MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
@@ -277,7 +283,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
                 10 /* start */, 16 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 2);
         MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
@@ -290,7 +296,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
                 5 /* start */, 16 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 1);
         MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
@@ -302,7 +308,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
                 10 /* start */, 20 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 1);
         MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
@@ -314,7 +320,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
                 5 /* start */, 20 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 1);
         MoreAsserts.assertEquals(suggestions[0].getSuggestions(), SUGGESTIONS1);
@@ -326,7 +332,7 @@
         text.setSpan(new SuggestionSpan(Locale.ENGLISH, SUGGESTIONS2, 0 /* flags */),
                 5 /* start */, 20 /* end */, 0 /* flags */);
         mockInputMethodService.setInputConnection(new MockConnection(text, cursorPos));
-        r = ic.getWordRangeAtCursor(" ", 0);
+        r = ic.getWordRangeAtCursor(SPACE, 0);
         suggestions = r.getSuggestionSpansAtWord();
         assertEquals(suggestions.length, 0);
     }
diff --git a/tests/src/com/android/inputmethod/latin/settings/SpacingAndPunctuationsTests.java b/tests/src/com/android/inputmethod/latin/settings/SpacingAndPunctuationsTests.java
new file mode 100644
index 0000000..424e7ff
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/settings/SpacingAndPunctuationsTests.java
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.settings;
+
+import android.content.res.Resources;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.utils.RunInLocale;
+
+import junit.framework.AssertionFailedError;
+
+import java.util.Locale;
+
+@SmallTest
+public class SpacingAndPunctuationsTests extends AndroidTestCase {
+    private static final int ARMENIAN_FULL_STOP = '\u0589';
+    private static final int ARMENIAN_COMMA = '\u055D';
+
+    private SpacingAndPunctuations ENGLISH;
+    private SpacingAndPunctuations FRENCH;
+    private SpacingAndPunctuations GERMAN;
+    private SpacingAndPunctuations ARMENIAN;
+    private SpacingAndPunctuations THAI;
+    private SpacingAndPunctuations KHMER;
+    private SpacingAndPunctuations LAO;
+    private SpacingAndPunctuations ARABIC;
+    private SpacingAndPunctuations PERSIAN;
+    private SpacingAndPunctuations HEBREW;
+
+    private SpacingAndPunctuations UNITED_STATES;
+    private SpacingAndPunctuations UNITED_KINGDOM;
+    private SpacingAndPunctuations CANADA_FRENCH;
+    private SpacingAndPunctuations SWISS_GERMAN;
+    private SpacingAndPunctuations INDIA_ENGLISH;
+    private SpacingAndPunctuations ARMENIA_ARMENIAN;
+    private SpacingAndPunctuations CAMBODIA_KHMER;
+    private SpacingAndPunctuations LAOS_LAO;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Language only
+        ENGLISH = getSpacingAndPunctuations(Locale.ENGLISH);
+        FRENCH = getSpacingAndPunctuations(Locale.FRENCH);
+        GERMAN = getSpacingAndPunctuations(Locale.GERMAN);
+        THAI = getSpacingAndPunctuations(new Locale("th"));
+        ARMENIAN = getSpacingAndPunctuations(new Locale("hy"));
+        KHMER = getSpacingAndPunctuations(new Locale("km"));
+        LAO = getSpacingAndPunctuations(new Locale("lo"));
+        ARABIC = getSpacingAndPunctuations(new Locale("ar"));
+        PERSIAN = getSpacingAndPunctuations(new Locale("fa"));
+        HEBREW = getSpacingAndPunctuations(new Locale("iw"));
+
+        // Language and Country
+        UNITED_STATES = getSpacingAndPunctuations(Locale.US);
+        UNITED_KINGDOM = getSpacingAndPunctuations(Locale.UK);
+        CANADA_FRENCH = getSpacingAndPunctuations(Locale.CANADA_FRENCH);
+        SWISS_GERMAN = getSpacingAndPunctuations(new Locale("de", "CH"));
+        INDIA_ENGLISH = getSpacingAndPunctuations(new Locale("en", "IN"));
+        ARMENIA_ARMENIAN = getSpacingAndPunctuations(new Locale("hy", "AM"));
+        CAMBODIA_KHMER = getSpacingAndPunctuations(new Locale("km", "KH"));
+        LAOS_LAO = getSpacingAndPunctuations(new Locale("lo", "LA"));
+    }
+
+    private SpacingAndPunctuations getSpacingAndPunctuations(final Locale locale) {
+        final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() {
+            @Override
+            protected SpacingAndPunctuations job(Resources res) {
+                return new SpacingAndPunctuations(res);
+            }
+        };
+        return job.runInLocale(getContext().getResources(), locale);
+    }
+
+    private static void testingStandardWordSeparator(final SpacingAndPunctuations sp) {
+        assertTrue("Tab",         sp.isWordSeparator('\t'));
+        assertTrue("Newline",     sp.isWordSeparator('\n'));
+        assertTrue("Space",       sp.isWordSeparator(' '));
+        assertTrue("Exclamation", sp.isWordSeparator('!'));
+        assertTrue("Quotation",   sp.isWordSeparator('"'));
+        assertFalse("Number",     sp.isWordSeparator('#'));
+        assertFalse("Dollar",     sp.isWordSeparator('$'));
+        assertFalse("Percent",    sp.isWordSeparator('%'));
+        assertTrue("Ampersand",   sp.isWordSeparator('&'));
+        assertFalse("Apostrophe", sp.isWordSeparator('\''));
+        assertTrue("L Paren",     sp.isWordSeparator('('));
+        assertTrue("R Paren",     sp.isWordSeparator(')'));
+        assertTrue("Asterisk",    sp.isWordSeparator('*'));
+        assertTrue("Plus",        sp.isWordSeparator('+'));
+        assertTrue("Comma",       sp.isWordSeparator(','));
+        assertFalse("Minus",      sp.isWordSeparator('-'));
+        assertTrue("Period",      sp.isWordSeparator('.'));
+        assertTrue("Slash",       sp.isWordSeparator('/'));
+        assertTrue("Colon",       sp.isWordSeparator(':'));
+        assertTrue("Semicolon",   sp.isWordSeparator(';'));
+        assertTrue("L Angle",     sp.isWordSeparator('<'));
+        assertTrue("Equal",       sp.isWordSeparator('='));
+        assertTrue("R Angle",     sp.isWordSeparator('>'));
+        assertTrue("Question",    sp.isWordSeparator('?'));
+        assertFalse("Atmark",     sp.isWordSeparator('@'));
+        assertTrue("L S Bracket", sp.isWordSeparator('['));
+        assertFalse("B Slash",    sp.isWordSeparator('\\'));
+        assertTrue("R S Bracket", sp.isWordSeparator(']'));
+        assertFalse("Circumflex", sp.isWordSeparator('^'));
+        assertTrue("Underscore",  sp.isWordSeparator('_'));
+        assertFalse("Grave",      sp.isWordSeparator('`'));
+        assertTrue("L C Brace",   sp.isWordSeparator('{'));
+        assertTrue("V Line",      sp.isWordSeparator('|'));
+        assertTrue("R C Brace",   sp.isWordSeparator('}'));
+        assertFalse("Tilde",      sp.isWordSeparator('~'));
+    }
+
+    public void testWordSeparator() {
+        testingStandardWordSeparator(ENGLISH);
+        testingStandardWordSeparator(FRENCH);
+        testingStandardWordSeparator(CANADA_FRENCH);
+        testingStandardWordSeparator(ARMENIA_ARMENIAN);
+        assertTrue(ARMENIA_ARMENIAN.isWordSeparator(ARMENIAN_FULL_STOP));
+        assertTrue(ARMENIA_ARMENIAN.isWordSeparator(ARMENIAN_COMMA));
+        // TODO: We should fix these.
+        testingStandardWordSeparator(ARMENIAN);
+        assertFalse(ARMENIAN.isWordSeparator(ARMENIAN_FULL_STOP));
+        assertFalse(ARMENIAN.isWordSeparator(ARMENIAN_COMMA));
+    }
+
+    private static void testingStandardWordConnector(final SpacingAndPunctuations sp) {
+        assertFalse("Tab",         sp.isWordConnector('\t'));
+        assertFalse("Newline",     sp.isWordConnector('\n'));
+        assertFalse("Space",       sp.isWordConnector(' '));
+        assertFalse("Exclamation", sp.isWordConnector('!'));
+        assertFalse("Quotation",   sp.isWordConnector('"'));
+        assertFalse("Number",      sp.isWordConnector('#'));
+        assertFalse("Dollar",      sp.isWordConnector('$'));
+        assertFalse("Percent",     sp.isWordConnector('%'));
+        assertFalse("Ampersand",   sp.isWordConnector('&'));
+        assertTrue("Apostrophe",   sp.isWordConnector('\''));
+        assertFalse("L Paren",     sp.isWordConnector('('));
+        assertFalse("R Paren",     sp.isWordConnector(')'));
+        assertFalse("Asterisk",    sp.isWordConnector('*'));
+        assertFalse("Plus",        sp.isWordConnector('+'));
+        assertFalse("Comma",       sp.isWordConnector(','));
+        assertTrue("Minus",        sp.isWordConnector('-'));
+        assertFalse("Period",      sp.isWordConnector('.'));
+        assertFalse("Slash",       sp.isWordConnector('/'));
+        assertFalse("Colon",       sp.isWordConnector(':'));
+        assertFalse("Semicolon",   sp.isWordConnector(';'));
+        assertFalse("L Angle",     sp.isWordConnector('<'));
+        assertFalse("Equal",       sp.isWordConnector('='));
+        assertFalse("R Angle",     sp.isWordConnector('>'));
+        assertFalse("Question",    sp.isWordConnector('?'));
+        assertFalse("Atmark",      sp.isWordConnector('@'));
+        assertFalse("L S Bracket", sp.isWordConnector('['));
+        assertFalse("B Slash",     sp.isWordConnector('\\'));
+        assertFalse("R S Bracket", sp.isWordConnector(']'));
+        assertFalse("Circumflex",  sp.isWordConnector('^'));
+        assertFalse("Underscore",  sp.isWordConnector('_'));
+        assertFalse("Grave",       sp.isWordConnector('`'));
+        assertFalse("L C Brace",   sp.isWordConnector('{'));
+        assertFalse("V Line",      sp.isWordConnector('|'));
+        assertFalse("R C Brace",   sp.isWordConnector('}'));
+        assertFalse("Tilde",       sp.isWordConnector('~'));
+
+    }
+
+    public void testWordConnector() {
+        testingStandardWordConnector(ENGLISH);
+        testingStandardWordConnector(FRENCH);
+        testingStandardWordConnector(CANADA_FRENCH);
+        testingStandardWordConnector(ARMENIA_ARMENIAN);
+    }
+
+    private static void testingCommonPrecededBySpace(final SpacingAndPunctuations sp) {
+        assertFalse("Tab",         sp.isUsuallyPrecededBySpace('\t'));
+        assertFalse("Newline",     sp.isUsuallyPrecededBySpace('\n'));
+        assertFalse("Space",       sp.isUsuallyPrecededBySpace(' '));
+        //assertFalse("Exclamation", sp.isUsuallyPrecededBySpace('!'));
+        assertFalse("Quotation",   sp.isUsuallyPrecededBySpace('"'));
+        assertFalse("Number",      sp.isUsuallyPrecededBySpace('#'));
+        assertFalse("Dollar",      sp.isUsuallyPrecededBySpace('$'));
+        assertFalse("Percent",     sp.isUsuallyPrecededBySpace('%'));
+        assertTrue("Ampersand",    sp.isUsuallyPrecededBySpace('&'));
+        assertFalse("Apostrophe",  sp.isUsuallyPrecededBySpace('\''));
+        assertTrue("L Paren",      sp.isUsuallyPrecededBySpace('('));
+        assertFalse("R Paren",     sp.isUsuallyPrecededBySpace(')'));
+        assertFalse("Asterisk",    sp.isUsuallyPrecededBySpace('*'));
+        assertFalse("Plus",        sp.isUsuallyPrecededBySpace('+'));
+        assertFalse("Comma",       sp.isUsuallyPrecededBySpace(','));
+        assertFalse("Minus",       sp.isUsuallyPrecededBySpace('-'));
+        assertFalse("Period",      sp.isUsuallyPrecededBySpace('.'));
+        assertFalse("Slash",       sp.isUsuallyPrecededBySpace('/'));
+        //assertFalse("Colon",       sp.isUsuallyPrecededBySpace(':'));
+        //assertFalse("Semicolon",   sp.isUsuallyPrecededBySpace(';'));
+        assertFalse("L Angle",     sp.isUsuallyPrecededBySpace('<'));
+        assertFalse("Equal",       sp.isUsuallyPrecededBySpace('='));
+        assertFalse("R Angle",     sp.isUsuallyPrecededBySpace('>'));
+        //assertFalse("Question",    sp.isUsuallyPrecededBySpace('?'));
+        assertFalse("Atmark",      sp.isUsuallyPrecededBySpace('@'));
+        assertTrue("L S Bracket",  sp.isUsuallyPrecededBySpace('['));
+        assertFalse("B Slash",     sp.isUsuallyPrecededBySpace('\\'));
+        assertFalse("R S Bracket", sp.isUsuallyPrecededBySpace(']'));
+        assertFalse("Circumflex",  sp.isUsuallyPrecededBySpace('^'));
+        assertFalse("Underscore",  sp.isUsuallyPrecededBySpace('_'));
+        assertFalse("Grave",       sp.isUsuallyPrecededBySpace('`'));
+        assertTrue("L C Brace",    sp.isUsuallyPrecededBySpace('{'));
+        assertFalse("V Line",      sp.isUsuallyPrecededBySpace('|'));
+        assertFalse("R C Brace",   sp.isUsuallyPrecededBySpace('}'));
+        assertFalse("Tilde",       sp.isUsuallyPrecededBySpace('~'));
+    }
+
+    private static void testingStandardPrecededBySpace(final SpacingAndPunctuations sp) {
+        testingCommonPrecededBySpace(sp);
+        assertFalse("Exclamation", sp.isUsuallyPrecededBySpace('!'));
+        assertFalse("Colon",       sp.isUsuallyPrecededBySpace(':'));
+        assertFalse("Semicolon",   sp.isUsuallyPrecededBySpace(';'));
+        assertFalse("Question",    sp.isUsuallyPrecededBySpace('?'));
+    }
+
+    public void testIsUsuallyPrecededBySpace() {
+        testingStandardPrecededBySpace(ENGLISH);
+        testingCommonPrecededBySpace(FRENCH);
+        assertTrue("Exclamation", FRENCH.isUsuallyPrecededBySpace('!'));
+        assertTrue("Colon",       FRENCH.isUsuallyPrecededBySpace(':'));
+        assertTrue("Semicolon",   FRENCH.isUsuallyPrecededBySpace(';'));
+        assertTrue("Question",    FRENCH.isUsuallyPrecededBySpace('?'));
+        testingCommonPrecededBySpace(CANADA_FRENCH);
+        assertFalse("Exclamation", CANADA_FRENCH.isUsuallyPrecededBySpace('!'));
+        assertTrue("Colon",        CANADA_FRENCH.isUsuallyPrecededBySpace(':'));
+        assertFalse("Semicolon",   CANADA_FRENCH.isUsuallyPrecededBySpace(';'));
+        assertFalse("Question",    CANADA_FRENCH.isUsuallyPrecededBySpace('?'));
+        testingStandardPrecededBySpace(ARMENIA_ARMENIAN);
+    }
+
+    private static void testingStandardFollowedBySpace(final SpacingAndPunctuations sp) {
+        assertFalse("Tab",         sp.isUsuallyFollowedBySpace('\t'));
+        assertFalse("Newline",     sp.isUsuallyFollowedBySpace('\n'));
+        assertFalse("Space",       sp.isUsuallyFollowedBySpace(' '));
+        assertTrue("Exclamation",  sp.isUsuallyFollowedBySpace('!'));
+        assertFalse("Quotation",   sp.isUsuallyFollowedBySpace('"'));
+        assertFalse("Number",      sp.isUsuallyFollowedBySpace('#'));
+        assertFalse("Dollar",      sp.isUsuallyFollowedBySpace('$'));
+        assertFalse("Percent",     sp.isUsuallyFollowedBySpace('%'));
+        assertTrue("Ampersand",    sp.isUsuallyFollowedBySpace('&'));
+        assertFalse("Apostrophe",  sp.isUsuallyFollowedBySpace('\''));
+        assertFalse("L Paren",     sp.isUsuallyFollowedBySpace('('));
+        assertTrue("R Paren",      sp.isUsuallyFollowedBySpace(')'));
+        assertFalse("Asterisk",    sp.isUsuallyFollowedBySpace('*'));
+        assertFalse("Plus",        sp.isUsuallyFollowedBySpace('+'));
+        assertTrue("Comma",        sp.isUsuallyFollowedBySpace(','));
+        assertFalse("Minus",       sp.isUsuallyFollowedBySpace('-'));
+        assertTrue("Period",       sp.isUsuallyFollowedBySpace('.'));
+        assertFalse("Slash",       sp.isUsuallyFollowedBySpace('/'));
+        assertTrue("Colon",        sp.isUsuallyFollowedBySpace(':'));
+        assertTrue("Semicolon",    sp.isUsuallyFollowedBySpace(';'));
+        assertFalse("L Angle",     sp.isUsuallyFollowedBySpace('<'));
+        assertFalse("Equal",       sp.isUsuallyFollowedBySpace('='));
+        assertFalse("R Angle",     sp.isUsuallyFollowedBySpace('>'));
+        assertTrue("Question",     sp.isUsuallyFollowedBySpace('?'));
+        assertFalse("Atmark",      sp.isUsuallyFollowedBySpace('@'));
+        assertFalse("L S Bracket", sp.isUsuallyFollowedBySpace('['));
+        assertFalse("B Slash",     sp.isUsuallyFollowedBySpace('\\'));
+        assertTrue("R S Bracket",  sp.isUsuallyFollowedBySpace(']'));
+        assertFalse("Circumflex",  sp.isUsuallyFollowedBySpace('^'));
+        assertFalse("Underscore",  sp.isUsuallyFollowedBySpace('_'));
+        assertFalse("Grave",       sp.isUsuallyFollowedBySpace('`'));
+        assertFalse("L C Brace",   sp.isUsuallyFollowedBySpace('{'));
+        assertFalse("V Line",      sp.isUsuallyFollowedBySpace('|'));
+        assertTrue("R C Brace",    sp.isUsuallyFollowedBySpace('}'));
+        assertFalse("Tilde",       sp.isUsuallyFollowedBySpace('~'));
+    }
+
+    public void testIsUsuallyFollowedBySpace() {
+        testingStandardFollowedBySpace(ENGLISH);
+        testingStandardFollowedBySpace(FRENCH);
+        testingStandardFollowedBySpace(CANADA_FRENCH);
+        testingStandardFollowedBySpace(ARMENIA_ARMENIAN);
+        assertTrue(ARMENIA_ARMENIAN.isUsuallyFollowedBySpace(ARMENIAN_FULL_STOP));
+        assertTrue(ARMENIA_ARMENIAN.isUsuallyFollowedBySpace(ARMENIAN_COMMA));
+    }
+
+    private static void testingStandardSentenceSeparator(final SpacingAndPunctuations sp) {
+        assertFalse("Tab",         sp.isUsuallyFollowedBySpace('\t'));
+        assertFalse("Newline",     sp.isUsuallyFollowedBySpace('\n'));
+        assertFalse("Space",       sp.isUsuallyFollowedBySpace(' '));
+        assertFalse("Exclamation", sp.isUsuallyFollowedBySpace('!'));
+        assertFalse("Quotation",   sp.isUsuallyFollowedBySpace('"'));
+        assertFalse("Number",      sp.isUsuallyFollowedBySpace('#'));
+        assertFalse("Dollar",      sp.isUsuallyFollowedBySpace('$'));
+        assertFalse("Percent",     sp.isUsuallyFollowedBySpace('%'));
+        assertFalse("Ampersand",   sp.isUsuallyFollowedBySpace('&'));
+        assertFalse("Apostrophe",  sp.isUsuallyFollowedBySpace('\''));
+        assertFalse("L Paren",     sp.isUsuallyFollowedBySpace('('));
+        assertFalse("R Paren",     sp.isUsuallyFollowedBySpace(')'));
+        assertFalse("Asterisk",    sp.isUsuallyFollowedBySpace('*'));
+        assertFalse("Plus",        sp.isUsuallyFollowedBySpace('+'));
+        assertFalse("Comma",       sp.isUsuallyFollowedBySpace(','));
+        assertFalse("Minus",       sp.isUsuallyFollowedBySpace('-'));
+        assertTrue("Period",       sp.isUsuallyFollowedBySpace('.'));
+        assertFalse("Slash",       sp.isUsuallyFollowedBySpace('/'));
+        assertFalse("Colon",       sp.isUsuallyFollowedBySpace(':'));
+        assertFalse("Semicolon",   sp.isUsuallyFollowedBySpace(';'));
+        assertFalse("L Angle",     sp.isUsuallyFollowedBySpace('<'));
+        assertFalse("Equal",       sp.isUsuallyFollowedBySpace('='));
+        assertFalse("R Angle",     sp.isUsuallyFollowedBySpace('>'));
+        assertFalse("Question",    sp.isUsuallyFollowedBySpace('?'));
+        assertFalse("Atmark",      sp.isUsuallyFollowedBySpace('@'));
+        assertFalse("L S Bracket", sp.isUsuallyFollowedBySpace('['));
+        assertFalse("B Slash",     sp.isUsuallyFollowedBySpace('\\'));
+        assertFalse("R S Bracket", sp.isUsuallyFollowedBySpace(']'));
+        assertFalse("Circumflex",  sp.isUsuallyFollowedBySpace('^'));
+        assertFalse("Underscore",  sp.isUsuallyFollowedBySpace('_'));
+        assertFalse("Grave",       sp.isUsuallyFollowedBySpace('`'));
+        assertFalse("L C Brace",   sp.isUsuallyFollowedBySpace('{'));
+        assertFalse("V Line",      sp.isUsuallyFollowedBySpace('|'));
+        assertFalse("R C Brace",   sp.isUsuallyFollowedBySpace('}'));
+        assertFalse("Tilde",       sp.isUsuallyFollowedBySpace('~'));
+    }
+
+    public void isSentenceSeparator() {
+        testingStandardSentenceSeparator(ENGLISH);
+        try {
+            testingStandardSentenceSeparator(ARMENIA_ARMENIAN);
+            fail("Armenian Sentence Separator");
+        } catch (final AssertionFailedError e) {
+            assertEquals("Period", e.getMessage());
+        }
+        assertTrue(ARMENIA_ARMENIAN.isSentenceSeparator(ARMENIAN_FULL_STOP));
+        assertFalse(ARMENIA_ARMENIAN.isSentenceSeparator(ARMENIAN_COMMA));
+    }
+
+    public void testLanguageHasSpace() {
+        assertTrue(ENGLISH.mCurrentLanguageHasSpaces);
+        assertTrue(FRENCH.mCurrentLanguageHasSpaces);
+        assertTrue(GERMAN.mCurrentLanguageHasSpaces);
+        assertFalse(THAI.mCurrentLanguageHasSpaces);
+        assertFalse(CAMBODIA_KHMER.mCurrentLanguageHasSpaces);
+        assertFalse(LAOS_LAO.mCurrentLanguageHasSpaces);
+        // TODO: We should fix these.
+        assertTrue(KHMER.mCurrentLanguageHasSpaces);
+        assertTrue(LAO.mCurrentLanguageHasSpaces);
+    }
+
+    public void testUsesAmericanTypography() {
+        assertTrue(ENGLISH.mUsesAmericanTypography);
+        assertTrue(UNITED_STATES.mUsesAmericanTypography);
+        assertTrue(UNITED_KINGDOM.mUsesAmericanTypography);
+        assertTrue(INDIA_ENGLISH.mUsesAmericanTypography);
+        assertFalse(FRENCH.mUsesAmericanTypography);
+        assertFalse(GERMAN.mUsesAmericanTypography);
+        assertFalse(SWISS_GERMAN.mUsesAmericanTypography);
+    }
+
+    public void testUsesGermanRules() {
+        assertFalse(ENGLISH.mUsesGermanRules);
+        assertFalse(FRENCH.mUsesGermanRules);
+        assertTrue(GERMAN.mUsesGermanRules);
+        assertTrue(SWISS_GERMAN.mUsesGermanRules);
+    }
+
+    private static void testingStandardPunctuationSuggestions(final SpacingAndPunctuations sp) {
+        final SuggestedWords suggestedWords = sp.mSuggestPuncList;
+        assertFalse("typedWordValid", suggestedWords.mTypedWordValid);
+        assertFalse("willAutoCorrect", suggestedWords.mWillAutoCorrect);
+        assertTrue("isPunctuationSuggestions", suggestedWords.mIsPunctuationSuggestions);
+        assertFalse("isObsoleteSuggestions", suggestedWords.mIsObsoleteSuggestions);
+        assertFalse("isPrediction", suggestedWords.mIsPrediction);
+        final String[] punctuations = {
+            "!", "?", ",", ":", ";", "\"", "(", ")", "'", "-", "/", "@", "_"
+        };
+        assertEquals("size", punctuations.length, suggestedWords.size());
+        for (int index = 0; index < punctuations.length; index++) {
+            assertEquals("punctuation at " + index,
+                    punctuations[index], suggestedWords.getWord(index));
+        }
+    }
+    public void testPunctuationSuggestions() {
+        testingStandardPunctuationSuggestions(ENGLISH);
+        testingStandardPunctuationSuggestions(FRENCH);
+        testingStandardPunctuationSuggestions(GERMAN);
+        // TODO: Should fix these RTL languages
+        testingStandardPunctuationSuggestions(ARABIC);
+        testingStandardPunctuationSuggestions(PERSIAN);
+        testingStandardPunctuationSuggestions(HEBREW);
+    }
+}
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);
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/RecapitalizeStatusTests.java b/tests/src/com/android/inputmethod/latin/utils/RecapitalizeStatusTests.java
index a520412..ada80c3 100644
--- a/tests/src/com/android/inputmethod/latin/utils/RecapitalizeStatusTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/RecapitalizeStatusTests.java
@@ -19,31 +19,35 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.inputmethod.latin.Constants;
+
 import java.util.Locale;
 
 @SmallTest
 public class RecapitalizeStatusTests extends AndroidTestCase {
+    private static final int[] SPACE = { Constants.CODE_SPACE };
+
     public void testTrim() {
         final RecapitalizeStatus status = new RecapitalizeStatus();
-        status.initialize(30, 40, "abcdefghij", Locale.ENGLISH, " ");
+        status.initialize(30, 40, "abcdefghij", Locale.ENGLISH, SPACE);
         status.trim();
         assertEquals("abcdefghij", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(40, status.getNewCursorEnd());
 
-        status.initialize(30, 44, "    abcdefghij", Locale.ENGLISH, " ");
+        status.initialize(30, 44, "    abcdefghij", Locale.ENGLISH, SPACE);
         status.trim();
         assertEquals("abcdefghij", status.getRecapitalizedString());
         assertEquals(34, status.getNewCursorStart());
         assertEquals(44, status.getNewCursorEnd());
 
-        status.initialize(30, 40, "abcdefgh  ", Locale.ENGLISH, " ");
+        status.initialize(30, 40, "abcdefgh  ", Locale.ENGLISH, SPACE);
         status.trim();
         assertEquals("abcdefgh", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(38, status.getNewCursorEnd());
 
-        status.initialize(30, 45, "   abcdefghij  ", Locale.ENGLISH, " ");
+        status.initialize(30, 45, "   abcdefghij  ", Locale.ENGLISH, SPACE);
         status.trim();
         assertEquals("abcdefghij", status.getRecapitalizedString());
         assertEquals(33, status.getNewCursorStart());
@@ -52,7 +56,7 @@
 
     public void testRotate() {
         final RecapitalizeStatus status = new RecapitalizeStatus();
-        status.initialize(29, 40, "abcd efghij", Locale.ENGLISH, " ");
+        status.initialize(29, 40, "abcd efghij", Locale.ENGLISH, SPACE);
         status.rotate();
         assertEquals("Abcd Efghij", status.getRecapitalizedString());
         assertEquals(29, status.getNewCursorStart());
@@ -64,7 +68,7 @@
         status.rotate();
         assertEquals("Abcd Efghij", status.getRecapitalizedString());
 
-        status.initialize(29, 40, "Abcd Efghij", Locale.ENGLISH, " ");
+        status.initialize(29, 40, "Abcd Efghij", Locale.ENGLISH, SPACE);
         status.rotate();
         assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
         assertEquals(29, status.getNewCursorStart());
@@ -76,7 +80,7 @@
         status.rotate();
         assertEquals("ABCD EFGHIJ", status.getRecapitalizedString());
 
-        status.initialize(29, 40, "ABCD EFGHIJ", Locale.ENGLISH, " ");
+        status.initialize(29, 40, "ABCD EFGHIJ", Locale.ENGLISH, SPACE);
         status.rotate();
         assertEquals("abcd efghij", status.getRecapitalizedString());
         assertEquals(29, status.getNewCursorStart());
@@ -88,7 +92,7 @@
         status.rotate();
         assertEquals("abcd efghij", status.getRecapitalizedString());
 
-        status.initialize(29, 39, "AbCDefghij", Locale.ENGLISH, " ");
+        status.initialize(29, 39, "AbCDefghij", Locale.ENGLISH, SPACE);
         status.rotate();
         assertEquals("abcdefghij", status.getRecapitalizedString());
         assertEquals(29, status.getNewCursorStart());
@@ -102,7 +106,7 @@
         status.rotate();
         assertEquals("abcdefghij", status.getRecapitalizedString());
 
-        status.initialize(29, 40, "Abcd efghij", Locale.ENGLISH, " ");
+        status.initialize(29, 40, "Abcd efghij", Locale.ENGLISH, SPACE);
         status.rotate();
         assertEquals("abcd efghij", status.getRecapitalizedString());
         assertEquals(29, status.getNewCursorStart());
@@ -116,7 +120,8 @@
         status.rotate();
         assertEquals("abcd efghij", status.getRecapitalizedString());
 
-        status.initialize(30, 34, "grüß", Locale.GERMAN, " "); status.rotate();
+        status.initialize(30, 34, "grüß", Locale.GERMAN, SPACE);
+        status.rotate();
         assertEquals("Grüß", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(34, status.getNewCursorEnd());
@@ -133,7 +138,8 @@
         assertEquals(30, status.getNewCursorStart());
         assertEquals(34, status.getNewCursorEnd());
 
-        status.initialize(30, 33, "œuf", Locale.FRENCH, " "); status.rotate();
+        status.initialize(30, 33, "œuf", Locale.FRENCH, SPACE);
+        status.rotate();
         assertEquals("Œuf", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(33, status.getNewCursorEnd());
@@ -150,7 +156,8 @@
         assertEquals(30, status.getNewCursorStart());
         assertEquals(33, status.getNewCursorEnd());
 
-        status.initialize(30, 33, "œUf", Locale.FRENCH, " "); status.rotate();
+        status.initialize(30, 33, "œUf", Locale.FRENCH, SPACE);
+        status.rotate();
         assertEquals("œuf", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(33, status.getNewCursorEnd());
@@ -171,7 +178,8 @@
         assertEquals(30, status.getNewCursorStart());
         assertEquals(33, status.getNewCursorEnd());
 
-        status.initialize(30, 35, "école", Locale.FRENCH, " "); status.rotate();
+        status.initialize(30, 35, "école", Locale.FRENCH, SPACE);
+        status.rotate();
         assertEquals("École", status.getRecapitalizedString());
         assertEquals(30, status.getNewCursorStart());
         assertEquals(35, status.getNewCursorEnd());
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
index 7196612..e55c32b 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
@@ -19,6 +19,8 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.inputmethod.latin.Constants;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
@@ -206,42 +208,47 @@
         assertTrue(StringUtils.isIdenticalAfterDowncase(""));
     }
 
-    private static void checkCapitalize(final String src, final String dst, final String separators,
-            final Locale locale) {
-        assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale));
+    private static void checkCapitalize(final String src, final String dst,
+            final int[] sortedSeparators, final Locale locale) {
+        assertEquals(dst, StringUtils.capitalizeEachWord(src, sortedSeparators, locale));
         assert(src.equals(dst)
-                == StringUtils.isIdenticalAfterCapitalizeEachWord(src, separators));
+                == StringUtils.isIdenticalAfterCapitalizeEachWord(src, sortedSeparators));
     }
 
+    private static final int[] SPACE = { Constants.CODE_SPACE };
+    private static final int[] SPACE_PERIOD = StringUtils.toSortedCodePointArray(" .");
+    private static final int[] SENTENCE_SEPARATORS =
+            StringUtils.toSortedCodePointArray(" \n.!?*()&");
+    private static final int[] WORD_SEPARATORS = StringUtils.toSortedCodePointArray(" \n.!?*,();&");
+
     public void testCapitalizeEachWord() {
-        checkCapitalize("", "", " ", Locale.ENGLISH);
-        checkCapitalize("test", "Test", " ", Locale.ENGLISH);
-        checkCapitalize("    test", "    Test", " ", Locale.ENGLISH);
-        checkCapitalize("Test", "Test", " ", Locale.ENGLISH);
-        checkCapitalize("    Test", "    Test", " ", Locale.ENGLISH);
-        checkCapitalize(".Test", ".test", " ", Locale.ENGLISH);
-        checkCapitalize(".Test", ".Test", " .", Locale.ENGLISH);
-        checkCapitalize(".Test", ".Test", ". ", Locale.ENGLISH);
-        checkCapitalize("test and retest", "Test And Retest", " .", Locale.ENGLISH);
-        checkCapitalize("Test and retest", "Test And Retest", " .", Locale.ENGLISH);
-        checkCapitalize("Test And Retest", "Test And Retest", " .", Locale.ENGLISH);
-        checkCapitalize("Test And.Retest  ", "Test And.Retest  ", " .", Locale.ENGLISH);
-        checkCapitalize("Test And.retest  ", "Test And.Retest  ", " .", Locale.ENGLISH);
-        checkCapitalize("Test And.retest  ", "Test And.retest  ", " ", Locale.ENGLISH);
-        checkCapitalize("Test And.Retest  ", "Test And.retest  ", " ", Locale.ENGLISH);
-        checkCapitalize("test and ietest", "Test And İetest", " .", new Locale("tr"));
-        checkCapitalize("test and ietest", "Test And Ietest", " .", Locale.ENGLISH);
-        checkCapitalize("Test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
-        checkCapitalize("Test&retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
-        checkCapitalize("test&Retest", "Test&Retest", " \n.!?*()&", Locale.ENGLISH);
+        checkCapitalize("", "", SPACE, Locale.ENGLISH);
+        checkCapitalize("test", "Test", SPACE, Locale.ENGLISH);
+        checkCapitalize("    test", "    Test", SPACE, Locale.ENGLISH);
+        checkCapitalize("Test", "Test", SPACE, Locale.ENGLISH);
+        checkCapitalize("    Test", "    Test", SPACE, Locale.ENGLISH);
+        checkCapitalize(".Test", ".test", SPACE, Locale.ENGLISH);
+        checkCapitalize(".Test", ".Test", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("test and retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test and retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test And Retest", "Test And Retest", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test And.Retest  ", "Test And.Retest  ", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test And.retest  ", "Test And.Retest  ", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test And.retest  ", "Test And.retest  ", SPACE, Locale.ENGLISH);
+        checkCapitalize("Test And.Retest  ", "Test And.retest  ", SPACE, Locale.ENGLISH);
+        checkCapitalize("test and ietest", "Test And İetest", SPACE_PERIOD, new Locale("tr"));
+        checkCapitalize("test and ietest", "Test And Ietest", SPACE_PERIOD, Locale.ENGLISH);
+        checkCapitalize("Test&Retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH);
+        checkCapitalize("Test&retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH);
+        checkCapitalize("test&Retest", "Test&Retest", SENTENCE_SEPARATORS, Locale.ENGLISH);
         checkCapitalize("rest\nrecreation! And in the end...",
-                "Rest\nRecreation! And In The End...", " \n.!?*,();&", Locale.ENGLISH);
+                "Rest\nRecreation! And In The End...", WORD_SEPARATORS, Locale.ENGLISH);
         checkCapitalize("lorem ipsum dolor sit amet", "Lorem Ipsum Dolor Sit Amet",
-                " \n.,!?*()&;", Locale.ENGLISH);
+                WORD_SEPARATORS, Locale.ENGLISH);
         checkCapitalize("Lorem!Ipsum (Dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet",
-                " \n,.;!?*()&", Locale.ENGLISH);
+                WORD_SEPARATORS, Locale.ENGLISH);
         checkCapitalize("Lorem!Ipsum (dolor) Sit * Amet", "Lorem!Ipsum (Dolor) Sit * Amet",
-                " \n,.;!?*()&", Locale.ENGLISH);
+                WORD_SEPARATORS, Locale.ENGLISH);
     }
 
     public void testLooksLikeURL() {