Merge "Boundary check for ProximityInfoState::getProximityTypeG()."
diff --git a/java/res/values/config-common.xml b/java/res/values/config-common.xml
index ab16a90..c920152 100644
--- a/java/res/values/config-common.xml
+++ b/java/res/values/config-common.xml
@@ -139,4 +139,8 @@
     <dimen name="config_accessibility_edge_slop">8dp</dimen>
 
     <integer name="config_user_dictionary_max_word_length">48</integer>
+
+    <!-- Personalization configuration -->
+    <!-- -1 means periocical wipe of the personalization dict is disabled. -->
+    <integer name="config_personalization_dict_wipe_interval_in_days">-1</integer>
 </resources>
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index f785835..7757d29 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -282,7 +282,9 @@
             @Override
             public void run() {
                 if (mDictionaryWriter == null) {
-                    mBinaryDictionary.close();
+                    if (mBinaryDictionary != null) {
+                        mBinaryDictionary.close();
+                    }
                     final File file = getDictFile();
                     if (file.exists() && !FileUtils.deleteRecursively(file)) {
                         Log.e(TAG, "Can't remove a file: " + file.getName());
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index aadb651..c9602bc 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -880,8 +880,9 @@
         // Notify ResearchLogger
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput,
-                    mInputLogic.mLastSelectionStart,
-                    mInputLogic.mLastSelectionEnd, getCurrentInputConnection());
+                    // TODO[IL]: mInputLogic.mConnection should be private
+                    mInputLogic.mConnection.getExpectedSelectionStart(),
+                    mInputLogic.mConnection.getExpectedSelectionEnd(), getCurrentInputConnection());
         }
     }
 
@@ -894,22 +895,18 @@
         if (DEBUG) {
             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
-                    + ", lss=" + mInputLogic.mLastSelectionStart
-                    + ", lse=" + mInputLogic.mLastSelectionEnd
                     + ", nss=" + newSelStart
                     + ", nse=" + newSelEnd
                     + ", cs=" + composingSpanStart
                     + ", ce=" + composingSpanEnd);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart,
-                    mInputLogic.mLastSelectionEnd,
+            ResearchLogger.latinIME_onUpdateSelection(oldSelStart, oldSelEnd,
                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
                     composingSpanEnd, mInputLogic.mConnection);
         }
 
-        final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart
-                || mInputLogic.mLastSelectionEnd != newSelEnd;
+        final boolean selectionChanged = oldSelStart != newSelStart || oldSelEnd != newSelEnd;
 
         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
         // span in the view - we can use that to narrow down whether the cursor was moved
@@ -972,9 +969,6 @@
             mKeyboardSwitcher.updateShiftState();
         }
 
-        // Make a note of the cursor position
-        mInputLogic.mLastSelectionStart = newSelStart;
-        mInputLogic.mLastSelectionEnd = newSelEnd;
         mSubtypeState.currentSubtypeUsed();
     }
 
@@ -1396,7 +1390,8 @@
                 // word. If we are composing a word we should have the second word before the cursor
                 // memorized, otherwise we should have the first.
                 final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion(
-                        currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1);
+                        currentSettings.mSpacingAndPunctuations,
+                        mInputLogic.mWordComposer.isComposingWord() ? 2 : 1);
                 if (!TextUtils.equals(previousWord, rereadPrevWord)) {
                     throw new RuntimeException("Unexpected previous word: "
                             + previousWord + " <> " + rereadPrevWord);
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 22377e0..79d6674 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -28,6 +28,7 @@
 
 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;
 import com.android.inputmethod.latin.utils.SpannableStringUtils;
@@ -173,13 +174,15 @@
         }
         final int lengthOfTextBeforeCursor = mCommittedTextBeforeComposingText.length();
         if (lengthOfTextBeforeCursor > newSelStart
-                || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
+                || (newSelStart != lengthOfTextBeforeCursor
+                        && lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE
                         && newSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
             // newSelStart and newSelEnd may be lying -- when rotating the device (probably a
-            // framework bug). If we have less chars than we asked for, then we know how many chars
-            // we have, and if we got more than newSelStart says, then we know it was lying. In both
-            // cases the length is more reliable.  Note that we only have to check newSelStart (not
-            // newSelEnd) since if newSelEnd is wrong, the newSelStart will be wrong as well.
+            // framework bug). If the values don't agree and we have less chars than we asked
+            // for, then we know how many chars we have. If we got more than newSelStart says, then
+            // we also know it was lying. In both cases the length is more reliable. Note that we
+            // only have to check newSelStart (not newSelEnd) since if newSelEnd is wrong, then
+            // newSelStart will be wrong as well.
             mExpectedSelStart = lengthOfTextBeforeCursor;
             mExpectedSelEnd = lengthOfTextBeforeCursor;
         }
@@ -537,7 +540,8 @@
     }
 
     @SuppressWarnings("unused")
-    public String getNthPreviousWord(final SettingsValues currentSettingsValues, final int n) {
+    public String getNthPreviousWord(final SpacingAndPunctuations spacingAndPunctuations,
+            final int n) {
         mIC = mParent.getCurrentInputConnection();
         if (null == mIC) return null;
         final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
@@ -556,7 +560,7 @@
                 }
             }
         }
-        return getNthPreviousWord(prev, currentSettingsValues, n);
+        return getNthPreviousWord(prev, spacingAndPunctuations, n);
     }
 
     private static boolean isSeparator(int code, String sep) {
@@ -580,7 +584,7 @@
     // (n = 2) "abc |" -> null
     // (n = 2) "abc. def|" -> null
     public static String getNthPreviousWord(final CharSequence prev,
-            final SettingsValues currentSettingsValues, final int n) {
+            final SpacingAndPunctuations spacingAndPunctuations, final int n) {
         if (prev == null) return null;
         final String[] w = spaceRegex.split(prev);
 
@@ -592,8 +596,8 @@
 
         // If ends in a separator, return null
         final char lastChar = nthPrevWord.charAt(length - 1);
-        if (currentSettingsValues.isWordSeparator(lastChar)
-                || currentSettingsValues.isWordConnector(lastChar)) return null;
+        if (spacingAndPunctuations.isWordSeparator(lastChar)
+                || spacingAndPunctuations.isWordConnector(lastChar)) return null;
 
         return nthPrevWord;
     }
@@ -781,17 +785,17 @@
      */
     public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart,
             final int oldSelEnd, final int newSelEnd) {
-        // This update is "belated" if we are expecting it.  That is, mExpectedSelStart and
+        // This update is "belated" if we are expecting it. That is, mExpectedSelStart and
         // mExpectedSelEnd match the new values that the TextView is updating TO.
         if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true;
-        // This update is not belated if mExpectedSelStart and mExpeectedSelend match the old
-        // values, and one of newSelStart or newSelEnd is updated to a different value.  In this
-        // case, there is likely something other than the IME that has moved the selection endpoint
+        // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old
+        // values, and one of newSelStart or newSelEnd is updated to a different value. In this
+        // case, there is likely something other than the IME has moved the selection endpoint
         // to the new value.
         if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd
                 && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false;
         // If nether of the above two cases holds, then the system may be having trouble keeping up
-        // with updates.  If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
+        // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart
         // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then
         // assume a belated update.
         return (newSelStart == newSelEnd)
@@ -809,4 +813,54 @@
     public boolean textBeforeCursorLooksLikeURL() {
         return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
     }
+
+    /**
+     * Try to get the text from the editor to expose lies the framework may have been
+     * telling us. Concretely, when the device rotates, the frameworks tells us about where the
+     * cursor used to be initially in the editor at the time it first received the focus; this
+     * may be completely different from the place it is upon rotation. Since we don't have any
+     * means to get the real value, try at least to ask the text view for some characters and
+     * detect the most damaging cases: when the cursor position is declared to be much smaller
+     * than it really is.
+     */
+    public void tryFixLyingCursorPosition() {
+        final CharSequence textBeforeCursor = getTextBeforeCursor(
+                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+        if (null == textBeforeCursor) {
+            mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION;
+        } else {
+            final int textLength = textBeforeCursor.length();
+            if (textLength > mExpectedSelStart
+                    || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
+                            && mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
+                // It should not be possible to have only one of those variables be
+                // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
+                // (simple cursor, no selection) or there is no cursor/we don't know its pos
+                final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd;
+                mExpectedSelStart = textLength;
+                // We can't figure out the value of mLastSelectionEnd :(
+                // But at least if it's smaller than mLastSelectionStart something is wrong,
+                // and if they used to be equal we also don't want to make it look like there is a
+                // selection.
+                if (wasEqual || mExpectedSelStart > mExpectedSelEnd) {
+                    mExpectedSelEnd = mExpectedSelStart;
+                }
+            }
+        }
+    }
+
+    public int getExpectedSelectionStart() {
+        return mExpectedSelStart;
+    }
+
+    public int getExpectedSelectionEnd() {
+        return mExpectedSelEnd;
+    }
+
+    /**
+     * @return whether there is a selection currently active.
+     */
+    public boolean hasSelection() {
+        return mExpectedSelEnd != mExpectedSelStart;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 968129a..7222b73 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -46,6 +46,7 @@
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.suggestions.SuggestionStripView;
 import com.android.inputmethod.latin.utils.AsyncResultHolder;
 import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -68,7 +69,8 @@
     // TODO : Remove this member when we can.
     private final LatinIME mLatinIME;
 
-    private InputLogicHandler mInputLogicHandler;
+    // Never null.
+    private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
 
     // TODO : make all these fields private as soon as possible.
     // Current space state of the input method. This can be any of the above constants.
@@ -84,10 +86,6 @@
     public final RichInputConnection mConnection;
     public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
 
-    // Keep track of the last selection range to decide if we need to show word alternatives
-    public int mLastSelectionStart = Constants.NOT_A_CURSOR_POSITION;
-    public int mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
-
     private int mDeleteCount;
     private long mLastKeyTime;
     public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
@@ -104,7 +102,7 @@
         mWordComposer = new WordComposer();
         mEventInterpreter = new EventInterpreter(latinIME);
         mConnection = new RichInputConnection(latinIME);
-        mInputLogicHandler = null;
+        mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
     }
 
     /**
@@ -127,11 +125,9 @@
         mRecapitalizeStatus.deactivate();
         mCurrentlyPressedHardwareKeys.clear();
         mSuggestedWords = SuggestedWords.EMPTY;
-        mLastSelectionStart = editorInfo.initialSelStart;
-        mLastSelectionEnd = editorInfo.initialSelEnd;
         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
         // so we try using some heuristics to find out about these and fix them.
-        tryFixLyingCursorPosition();
+        mConnection.tryFixLyingCursorPosition();
         mInputLogicHandler = new InputLogicHandler(mLatinIME, this);
     }
 
@@ -144,7 +140,7 @@
         }
         resetComposingState(true /* alsoResetLastComposedWord */);
         mInputLogicHandler.destroy();
-        mInputLogicHandler = null;
+        mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
     }
 
     /**
@@ -335,7 +331,8 @@
             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
                 // If we are in the middle of a recorrection, we need to commit the recorrection
                 // first so that we can insert the batch input at the current cursor position.
-                resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+                resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                        mConnection.getExpectedSelectionEnd());
             } else if (wordComposerSize <= 1) {
                 // We auto-correct the previous (typed, not gestured) string iff it's one character
                 // long. The reason for this is, even in the middle of gesture typing, you'll still
@@ -369,7 +366,8 @@
         mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
                 getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()),
                 // Prev word is 1st word before cursor
-                getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
+                getNthPreviousWordForSuggestion(
+                        settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */));
     }
 
     /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
@@ -463,7 +461,8 @@
                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
                     // If we are in the middle of a recorrection, we need to commit the recorrection
                     // first so that we can insert the character at the current cursor position.
-                    resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+                    resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                            mConnection.getExpectedSelectionEnd());
                 } else {
                     commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
                 }
@@ -513,7 +512,8 @@
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can insert the character at the current cursor position.
-            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+            resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                    mConnection.getExpectedSelectionEnd());
             isComposingWord = false;
         }
         // We want to find out whether to start composing a new word with this character. If so,
@@ -555,7 +555,8 @@
                 // yet, so the word we want is the 1st word before the cursor.
                 mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
                         getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()),
-                        getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
+                        getNthPreviousWordForSuggestion(
+                                settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */));
             }
             mConnection.setComposingText(getTextWithUnderline(
                     mWordComposer.getTypedWord()), 1);
@@ -599,7 +600,8 @@
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can insert the separator at the current cursor position.
-            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+            resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                    mConnection.getExpectedSelectionEnd());
         }
         // isComposingWord() may have changed since we stored wasComposing
         if (mWordComposer.isComposingWord()) {
@@ -691,7 +693,8 @@
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can remove the character at the current cursor position.
-            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+            resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                    mConnection.getExpectedSelectionEnd());
             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
         }
         if (mWordComposer.isComposingWord()) {
@@ -751,15 +754,12 @@
 
             // No cancelling of commit/double space/swap: we have a regular backspace.
             // We should backspace one char and restart suggestion if at the end of a word.
-            if (mLastSelectionStart != mLastSelectionEnd) {
+            if (mConnection.hasSelection()) {
                 // If there is a selection, remove it.
-                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
-                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
-                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
-                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
-                // but we want to set it right away to avoid it being used with the wrong values
-                // later (typically, in a subsequent press on backspace).
-                mLastSelectionEnd = mLastSelectionStart;
+                final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
+                        - mConnection.getExpectedSelectionStart();
+                mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
+                        mConnection.getExpectedSelectionEnd());
                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
                 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                     ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
@@ -767,7 +767,7 @@
                 }
             } else {
                 // There is no selection, just delete one character.
-                if (Constants.NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
+                if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
                     // This should never happen.
                     Log.e(TAG, "Backspace when we don't know the selection position");
                 }
@@ -956,38 +956,32 @@
      * @param settingsValues The current settings values.
      */
     private void performRecapitalization(final SettingsValues settingsValues) {
-        if (mLastSelectionStart == mLastSelectionEnd) {
+        if (!mConnection.hasSelection()) {
             return; // No selection
         }
         // If we have a recapitalize in progress, use it; otherwise, create a new one.
         if (!mRecapitalizeStatus.isActive()
-                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+                || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
+                        mConnection.getExpectedSelectionEnd())) {
             final CharSequence selectedText =
                     mConnection.getSelectedText(0 /* flags, 0 for no styles */);
             if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
-            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
-                    selectedText.toString(),
+            mRecapitalizeStatus.initialize(mConnection.getExpectedSelectionStart(),
+                    mConnection.getExpectedSelectionEnd(), selectedText.toString(),
                     settingsValues.mLocale, settingsValues.mSpacingAndPunctuations.mWordSeparators);
             // We trim leading and trailing whitespace.
             mRecapitalizeStatus.trim();
-            // Trimming the object may have changed the length of the string, and we need to
-            // reposition the selection handles accordingly. As this result in an IPC call,
-            // only do it if it's actually necessary, in other words if the recapitalize status
-            // is not set at the same place as before.
-            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
-                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
-                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
-            }
         }
         mConnection.finishComposingText();
         mRecapitalizeStatus.rotate();
-        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
-        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+        final int numCharsDeleted = mConnection.getExpectedSelectionEnd()
+                - mConnection.getExpectedSelectionStart();
+        mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
+                mConnection.getExpectedSelectionEnd());
         mConnection.deleteSurroundingText(numCharsDeleted, 0);
         mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
-        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
-        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
-        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+        mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
+                mRecapitalizeStatus.getNewCursorEnd());
     }
 
     private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues,
@@ -1068,10 +1062,10 @@
         // how to segment them yet.
         if (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) return;
         // If the cursor is not touching a word, or if there is a selection, return right away.
-        if (mLastSelectionStart != mLastSelectionEnd) return;
+        if (mConnection.hasSelection()) return;
         // If we don't know the cursor location, return.
-        if (mLastSelectionStart < 0) return;
-        final int expectedCursorPosition = mLastSelectionStart + offset; // We know Start == End
+        if (mConnection.getExpectedSelectionStart() < 0) return;
+        final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
         if (!mConnection.isCursorTouchingWord(settingsValues)) return;
         final TextRange range = mConnection.getWordRangeAtCursor(
                 settingsValues.mSpacingAndPunctuations.mWordSeparators,
@@ -1107,7 +1101,7 @@
             }
         }
         mWordComposer.setComposingWord(typedWord,
-                getNthPreviousWordForSuggestion(settingsValues,
+                getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations,
                         // We want the previous word for suggestion. If we have chars in the word
                         // before the cursor, then we want the word before that, hence 2; otherwise,
                         // we want the word immediately before the cursor, hence 1.
@@ -1285,7 +1279,8 @@
 
     public int getCurrentRecapitalizeState() {
         if (!mRecapitalizeStatus.isActive()
-                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+                || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(),
+                        mConnection.getExpectedSelectionEnd())) {
             // Not recapitalizing at the moment
             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
         }
@@ -1301,17 +1296,17 @@
 
     /**
      * Get the nth previous word before the cursor as context for the suggestion process.
-     * @param currentSettings the current settings values.
+     * @param spacingAndPunctuations the current spacing and punctuations settings.
      * @param nthPreviousWord reverse index of the word to get (1-indexed)
      * @return the nth previous word before the cursor.
      */
     // TODO: Make this private
-    public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
-            final int nthPreviousWord) {
-        if (currentSettings.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
+    public String getNthPreviousWordForSuggestion(
+            final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) {
+        if (spacingAndPunctuations.mCurrentLanguageHasSpaces) {
             // If we are typing in a language with spaces we can just look up the previous
             // word from textview.
-            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
+            return mConnection.getNthPreviousWord(spacingAndPunctuations, nthPreviousWord);
         } else {
             return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
                     : mLastComposedWord.mCommittedWord;
@@ -1638,7 +1633,8 @@
                 // of the auto-correction flash. At this moment, the "typedWord" argument is
                 // ignored by TextView.
                 mConnection.commitCorrection(
-                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+                        new CorrectionInfo(
+                        mConnection.getExpectedSelectionEnd() - typedWord.length(),
                         typedWord, autoCorrection));
             }
         }
@@ -1659,7 +1655,8 @@
         mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
                 suggestedWords), 1);
         // TODO: we pass 2 here, but would it be better to move this above and pass 1 instead?
-        final String prevWord = mConnection.getNthPreviousWord(settingsValues, 2);
+        final String prevWord = mConnection.getNthPreviousWord(
+                settingsValues.mSpacingAndPunctuations, 2);
         // Add the word to the user history dictionary
         performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWord);
         // TODO: figure out here if this is an auto-correct or if the best word is actually
@@ -1686,41 +1683,6 @@
     }
 
     /**
-     * Try to get the text from the editor to expose lies the framework may have been
-     * telling us. Concretely, when the device rotates, the frameworks tells us about where the
-     * cursor used to be initially in the editor at the time it first received the focus; this
-     * may be completely different from the place it is upon rotation. Since we don't have any
-     * means to get the real value, try at least to ask the text view for some characters and
-     * detect the most damaging cases: when the cursor position is declared to be much smaller
-     * than it really is.
-     */
-    private void tryFixLyingCursorPosition() {
-        final CharSequence textBeforeCursor = mConnection.getTextBeforeCursor(
-                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
-        if (null == textBeforeCursor) {
-            mLastSelectionStart = mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
-        } else {
-            final int textLength = textBeforeCursor.length();
-            if (textLength > mLastSelectionStart
-                    || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
-                            && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
-                // It should not be possible to have only one of those variables be
-                // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
-                // (simple cursor, no selection) or there is no cursor/we don't know its pos
-                final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
-                mLastSelectionStart = textLength;
-                // We can't figure out the value of mLastSelectionEnd :(
-                // But at least if it's smaller than mLastSelectionStart something is wrong,
-                // and if they used to be equal we also don't want to make it look like there is a
-                // selection.
-                if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
-                    mLastSelectionEnd = mLastSelectionStart;
-                }
-            }
-        }
-    }
-
-    /**
      * Retry resetting caches in the rich input connection.
      *
      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
@@ -1737,7 +1699,8 @@
             // TODO: remove these arguments
             final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(
-                mLastSelectionStart, mLastSelectionEnd, false)) {
+                mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(),
+                false)) {
             if (0 < remainingTries) {
                 handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
                 return;
@@ -1745,7 +1708,7 @@
             // If remainingTries is 0, we should stop waiting for new tries, but it's still
             // better to load the keyboard (less things will be broken).
         }
-        tryFixLyingCursorPosition();
+        mConnection.tryFixLyingCursorPosition();
         keyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), settingsValues);
         if (tryResumeSuggestions) {
             handler.postResumeSuggestions();
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
index 3258dcd..ea010b6 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
@@ -40,6 +40,33 @@
 
     private static final int MSG_GET_SUGGESTED_WORDS = 1;
 
+    // A handler that never does anything. This is used for cases where events come before anything
+    // is initialized, though probably only the monkey can actually do this.
+    public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() {
+        @Override
+        public void destroy() {}
+        @Override
+        public boolean handleMessage(final Message msg) { return true; }
+        @Override
+        public void onStartBatchInput() {}
+        @Override
+        public void onUpdateBatchInput(final InputPointers batchPointers,
+                final int sequenceNumber) {}
+        @Override
+        public void onCancelBatchInput() {}
+        @Override
+        public void onEndBatchInput(final InputPointers batchPointers, final int sequenceNumber) {}
+        @Override
+        public void getSuggestedWords(final int sessionId, final int sequenceNumber,
+                final OnGetSuggestedWordsCallback callback) {}
+    };
+
+    private InputLogicHandler() {
+        mNonUIThreadHandler = null;
+        mLatinIME = null;
+        mInputLogic = null;
+    }
+
     public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) {
         final HandlerThread handlerThread = new HandlerThread(
                 InputLogicHandler.class.getSimpleName());
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index add983b..323e612 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -93,6 +93,8 @@
 
     private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
             "pref_last_used_personalization_token";
+    private static final String PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME =
+            "pref_last_used_personalization_dict_wiped_time";
     public static final String PREF_SEND_FEEDBACK = "send_feedback";
     public static final String PREF_ABOUT_KEYBOARD = "about_keyboard";
 
@@ -372,6 +374,14 @@
         return StringUtils.hexStringToByteArray(tokenStr);
     }
 
+    public void writeLastPersonalizationDictWipedTime(final long timestamp) {
+        mPrefs.edit().putLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, timestamp).apply();
+    }
+
+    public long readLastPersonalizationDictGeneratedTime() {
+        return mPrefs.getLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, 0);
+    }
+
     public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) {
         prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply();
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 6ecee81..e4ae64f 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -24,7 +24,6 @@
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
-import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.compat.AppWorkaroundsUtils;
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.R;
@@ -166,53 +165,6 @@
         }
     }
 
-    // TODO: Remove this constructor.
-    // Only for tests
-    private SettingsValues(final Locale locale) {
-        // TODO: locale is saved, but not used yet. May have to change this if tests require.
-        mLocale = locale;
-        mDelayUpdateOldSuggestions = 0;
-        mSpacingAndPunctuations = new SpacingAndPunctuations(locale);
-        mHintToSaveText = "Touch again to save";
-        mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
-        mAutoCap = true;
-        mVibrateOn = true;
-        mSoundOn = true;
-        mKeyPreviewPopupOn = true;
-        mSlidingKeyInputPreviewEnabled = true;
-        mShowsVoiceInputKey = true;
-        mIncludesOtherImesInLanguageSwitchList = false;
-        mShowsLanguageSwitchKey = true;
-        mUseContactsDict = true;
-        mUsePersonalizedDicts = true;
-        mUseDoubleSpacePeriod = true;
-        mBlockPotentiallyOffensive = true;
-        mAutoCorrectEnabled = true;
-        mBigramPredictionEnabled = true;
-        mKeyLongpressTimeout = 300;
-        mKeypressVibrationDuration = 5;
-        mKeypressSoundVolume = 1;
-        mKeyPreviewPopupDismissDelay = 70;
-        mAutoCorrectionThreshold = 1;
-        mGestureInputEnabled = true;
-        mGestureTrailEnabled = true;
-        mGestureFloatingPreviewTextEnabled = true;
-        mPhraseGestureEnabled = true;
-        mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
-        mSuggestionVisibility = 0;
-        mIsInternal = false;
-        mUseOnlyPersonalizationDictionaryForDebug = false;
-        mDisplayOrientation = Configuration.ORIENTATION_PORTRAIT;
-        mAppWorkarounds = new AsyncResultHolder<AppWorkaroundsUtils>();
-        mAppWorkarounds.set(null);
-    }
-
-    // TODO: Remove this method.
-    @UsedForTesting
-    public static SettingsValues makeDummySettingsValuesForTest(final Locale locale) {
-        return new SettingsValues(locale);
-    }
-
     public boolean isApplicationSpecifiedCompletionsOn() {
         return mInputAttributes.mApplicationSpecifiedCompletionOn;
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 124c975..dbe30e2 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -18,7 +18,6 @@
 
 import android.content.res.Resources;
 
-import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
@@ -43,24 +42,6 @@
     public final boolean mCurrentLanguageHasSpaces;
     public final boolean mUsesAmericanTypography;
 
-    // TODO: Remove this constructor.
-    @UsedForTesting
-    SpacingAndPunctuations(final Locale locale) {
-        mSymbolsPrecededBySpace = new int[] { '(', '[', '{', '&' };
-        Arrays.sort(mSymbolsPrecededBySpace);
-        mSymbolsFollowedBySpace = new int[] { '.', ',', ';', ':', '!', '?', ')', ']', '}', '&' };
-        Arrays.sort(mSymbolsFollowedBySpace);
-        mWordConnectors = new int[] { '\'', '-' };
-        Arrays.sort(mWordConnectors);
-        mSentenceSeparator = Constants.CODE_PERIOD;
-        mSentenceSeparatorAndSpace = ". ";
-        final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" };
-        mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
-        mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\"";
-        mCurrentLanguageHasSpaces = true;
-        mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage());
-    }
-
     public SpacingAndPunctuations(final Resources res) {
         mSymbolsPrecededBySpace =
                 StringUtils.toCodePointArray(res.getString(R.string.symbols_preceded_by_space));
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 3daa63f..b3c787e 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -20,13 +20,16 @@
 import android.content.Context;
 import android.content.res.AssetManager;
 import android.content.res.Resources;
+import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.AssetFileAddress;
 import com.android.inputmethod.latin.BinaryDictionaryGetter;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -364,4 +367,29 @@
 
         return dictList;
     }
+
+    @UsedForTesting
+    public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+            final SpacingAndPunctuations spacingAndPunctuations) {
+        if (TextUtils.isEmpty(text)) return false;
+        final int length = text.length();
+        int i = 0;
+        int digitCount = 0;
+        while (i < length) {
+            final int codePoint = Character.codePointAt(text, i);
+            final int charCount = Character.charCount(codePoint);
+            i += charCount;
+            if (Character.isDigit(codePoint)) {
+                // Count digits: see below
+                digitCount += charCount;
+                continue;
+            }
+            if (!spacingAndPunctuations.isWordCodePoint(codePoint)) return false;
+        }
+        // We reject strings entirely comprised of digits to avoid using PIN codes or credit
+        // card numbers. It would come in handy for word prediction though; a good example is
+        // when writing one's address where the street number is usually quite discriminative,
+        // as well as the postal code.
+        return digitCount < length;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 85f4454..c5ed393 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -17,15 +17,11 @@
 package com.android.inputmethod.latin.utils;
 
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.settings.SettingsValues;
 
-import java.io.IOException;
 import java.util.ArrayList;
-import java.util.List;
 import java.util.Locale;
 
 public final class StringUtils {
@@ -268,31 +264,6 @@
         return true;
     }
 
-    @UsedForTesting
-    public static boolean looksValidForDictionaryInsertion(final CharSequence text,
-            final SettingsValues settings) {
-        if (TextUtils.isEmpty(text)) return false;
-        final int length = text.length();
-        int i = 0;
-        int digitCount = 0;
-        while (i < length) {
-            final int codePoint = Character.codePointAt(text, i);
-            final int charCount = Character.charCount(codePoint);
-            i += charCount;
-            if (Character.isDigit(codePoint)) {
-                // Count digits: see below
-                digitCount += charCount;
-                continue;
-            }
-            if (!settings.isWordCodePoint(codePoint)) return false;
-        }
-        // We reject strings entirely comprised of digits to avoid using PIN codes or credit
-        // card numbers. It would come in handy for word prediction though; a good example is
-        // when writing one's address where the street number is usually quite discriminative,
-        // as well as the postal code.
-        return digitCount < length;
-    }
-
     public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
             final String separators) {
         boolean needCapsNext = true;
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
index 6ad1250..f19d185 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
@@ -16,16 +16,13 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.latin.settings.SettingsValues;
-import com.android.inputmethod.latin.utils.TextRange;
-
+import android.content.res.Resources;
 import android.inputmethodservice.InputMethodService;
 import android.os.Parcel;
 import android.test.AndroidTestCase;
 import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.SpannableString;
-import android.text.Spanned;
 import android.text.TextUtils;
 import android.text.style.SuggestionSpan;
 import android.view.inputmethod.ExtractedText;
@@ -33,6 +30,10 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputConnectionWrapper;
 
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import com.android.inputmethod.latin.utils.RunInLocale;
+import com.android.inputmethod.latin.utils.TextRange;
+
 import java.util.Locale;
 
 @SmallTest
@@ -40,12 +41,19 @@
 
     // The following is meant to be a reasonable default for
     // the "word_separators" resource.
-    private static final SettingsValues sSettings =
-            SettingsValues.makeDummySettingsValuesForTest(Locale.ENGLISH);
+    private SpacingAndPunctuations mSpacingAndPunctuations;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+        final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() {
+            @Override
+            protected SpacingAndPunctuations job(final Resources res) {
+                return new SpacingAndPunctuations(res);
+            }
+        };
+        final Resources res = getContext().getResources();
+        mSpacingAndPunctuations = job.runInLocale(res, Locale.ENGLISH);
     }
 
     private class MockConnection extends InputConnectionWrapper {
@@ -139,9 +147,12 @@
      */
     public void testGetPreviousWord() {
         // If one of the following cases breaks, the bigram suggestions won't work.
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSettings, 2), "abc");
-        assertNull(RichInputConnection.getNthPreviousWord("abc", sSettings, 2));
-        assertNull(RichInputConnection.getNthPreviousWord("abc. def", sSettings, 2));
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def", mSpacingAndPunctuations, 2), "abc");
+        assertNull(RichInputConnection.getNthPreviousWord(
+                "abc", mSpacingAndPunctuations, 2));
+        assertNull(RichInputConnection.getNthPreviousWord(
+                "abc. def", mSpacingAndPunctuations, 2));
 
         // The following tests reflect the current behavior of the function
         // RichInputConnection#getNthPreviousWord.
@@ -150,15 +161,23 @@
         // this function if needed - especially since it does not seem very
         // logical. These tests are just there to catch any unintentional
         // changes in the behavior of the RichInputConnection#getPreviousWord method.
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSettings, 2), "abc");
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def.", sSettings, 2), "abc");
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def .", sSettings, 2), "def");
-        assertNull(RichInputConnection.getNthPreviousWord("abc ", sSettings, 2));
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def ", mSpacingAndPunctuations, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def.", mSpacingAndPunctuations, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def .", mSpacingAndPunctuations, 2), "def");
+        assertNull(RichInputConnection.getNthPreviousWord(
+                "abc ", mSpacingAndPunctuations, 2));
 
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSettings, 1), "def");
-        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSettings, 1), "def");
-        assertNull(RichInputConnection.getNthPreviousWord("abc def.", sSettings, 1));
-        assertNull(RichInputConnection.getNthPreviousWord("abc def .", sSettings, 1));
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def", mSpacingAndPunctuations, 1), "def");
+        assertEquals(RichInputConnection.getNthPreviousWord(
+                "abc def ", mSpacingAndPunctuations, 1), "def");
+        assertNull(RichInputConnection.getNthPreviousWord(
+                "abc def.", mSpacingAndPunctuations, 1));
+        assertNull(RichInputConnection.getNthPreviousWord(
+                "abc def .", mSpacingAndPunctuations, 1));
     }
 
     /**
diff --git a/tests/src/com/android/inputmethod/latin/utils/DictionaryInfoUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/DictionaryInfoUtilsTests.java
new file mode 100644
index 0000000..6e71607
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/DictionaryInfoUtilsTests.java
@@ -0,0 +1,47 @@
+/*
+ * 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.utils;
+
+import android.content.res.Resources;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+
+import java.util.Locale;
+
+@SmallTest
+public class DictionaryInfoUtilsTests extends AndroidTestCase {
+    public void testLooksValidForDictionaryInsertion() {
+        final RunInLocale<SpacingAndPunctuations> job = new RunInLocale<SpacingAndPunctuations>() {
+            @Override
+            protected SpacingAndPunctuations job(final Resources res) {
+                return new SpacingAndPunctuations(res);
+            }
+        };
+        final Resources res = getContext().getResources();
+        final SpacingAndPunctuations sp = job.runInLocale(res, Locale.ENGLISH);
+        assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("aochaueo", sp));
+        assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("", sp));
+        assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("ao-ch'aueo", sp));
+        assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("2908743256", sp));
+        assertTrue(DictionaryInfoUtils.looksValidForDictionaryInsertion("31aochaueo", sp));
+        assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("akeo  raeoch oerch .",
+                sp));
+        assertFalse(DictionaryInfoUtils.looksValidForDictionaryInsertion("!!!", sp));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
index 0c88f34..7196612 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.latin.utils;
 
-import com.android.inputmethod.latin.settings.SettingsValues;
-
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -208,18 +206,6 @@
         assertTrue(StringUtils.isIdenticalAfterDowncase(""));
     }
 
-    public void testLooksValidForDictionaryInsertion() {
-        final SettingsValues settings =
-                SettingsValues.makeDummySettingsValuesForTest(Locale.ENGLISH);
-        assertTrue(StringUtils.looksValidForDictionaryInsertion("aochaueo", settings));
-        assertFalse(StringUtils.looksValidForDictionaryInsertion("", settings));
-        assertTrue(StringUtils.looksValidForDictionaryInsertion("ao-ch'aueo", settings));
-        assertFalse(StringUtils.looksValidForDictionaryInsertion("2908743256", settings));
-        assertTrue(StringUtils.looksValidForDictionaryInsertion("31aochaueo", settings));
-        assertFalse(StringUtils.looksValidForDictionaryInsertion("akeo  raeoch oerch .", settings));
-        assertFalse(StringUtils.looksValidForDictionaryInsertion("!!!", settings));
-    }
-
     private static void checkCapitalize(final String src, final String dst, final String separators,
             final Locale locale) {
         assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale));
diff --git a/tools/dicttool/compat/com/android/inputmethod/latin/settings/SettingsValues.java b/tools/dicttool/compat/com/android/inputmethod/latin/settings/SettingsValues.java
deleted file mode 100644
index 0a84cde..0000000
--- a/tools/dicttool/compat/com/android/inputmethod/latin/settings/SettingsValues.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2013 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;
-
-public class SettingsValues {
-    public boolean isWordCodePoint(final int code) {
-        return Character.isLetter(code);
-    }
-}