Merge "Don't distrust the cursor pos so much as to bug on IPC delay"
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
index 138a626..43d4ba4 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
@@ -237,6 +237,24 @@
         mLatchForWaitingLoadingMainDictionary.await(timeout, unit);
     }
 
+    @UsedForTesting
+    public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)
+            throws InterruptedException {
+        waitForLoadingMainDictionary(timeout, unit);
+        if (mContactsDictionary != null) {
+            mContactsDictionary.waitAllTasksForTests();
+        }
+        if (mUserDictionary != null) {
+            mUserDictionary.waitAllTasksForTests();
+        }
+        if (mUserHistoryDictionary != null) {
+            mUserHistoryDictionary.waitAllTasksForTests();
+        }
+        if (mPersonalizationDictionary != null) {
+            mPersonalizationDictionary.waitAllTasksForTests();
+        }
+    }
+
     private void setMainDictionary(final Dictionary mainDictionary) {
         mMainDictionary = mainDictionary;
         addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDictionary);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 3b9be43..230739d 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -62,7 +62,7 @@
     private static final boolean DBG_STRESS_TEST = false;
 
     private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
-    private static final int TIMEOUT_FOR_READ_OPS_FOR_TESTS_IN_MILLISECONDS = 1000;
+    private static final int TIMEOUT_FOR_READ_OPS_FOR_TESTS_IN_MILLISECONDS = 10000;
 
     /**
      * The maximum length of a word in this dictionary.
@@ -750,7 +750,7 @@
     @UsedForTesting
     public boolean isInUnderlyingBinaryDictionaryForTests(final String word) {
         final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
-        getExecutor(mDictName).executePrioritized(new Runnable() {
+        getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
                 if (mDictType == Dictionary.TYPE_USER_HISTORY) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 29ff0ad..47137e7 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -541,7 +541,7 @@
             shouldKeepUserHistoryDictionaries = true;
             // TODO: Eliminate this restriction
             shouldKeepPersonalizationDictionaries =
-                    mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypes();
+                    mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes();
         } else {
             shouldKeepUserHistoryDictionaries = false;
             shouldKeepPersonalizationDictionaries = false;
@@ -1720,9 +1720,10 @@
 
     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
     @UsedForTesting
-    /* package for test */ void waitForMainDictionary(final long timeout, final TimeUnit unit)
+    /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit)
             throws InterruptedException {
-        mInputLogic.mSuggest.mDictionaryFacilitator.waitForLoadingMainDictionary(timeout, unit);
+        mInputLogic.mSuggest.mDictionaryFacilitator.waitForLoadingDictionariesForTesting(
+                timeout, unit);
     }
 
     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
@@ -1736,6 +1737,13 @@
         resetSuggest(new Suggest(locale, dictionaryFacilitator));
     }
 
+    // DO NOT USE THIS for any other purpose than testing.
+    @UsedForTesting
+    /* package for test */ void clearPersonalizedDictionariesForTest() {
+        mInputLogic.mSuggest.mDictionaryFacilitator.clearUserHistoryDictionary();
+        mInputLogic.mSuggest.mDictionaryFacilitator.clearPersonalizationDictionary();
+    }
+
     public void dumpDictionaryForDebug(final String dictName) {
         if (mInputLogic.mSuggest == null) {
             initSuggest();
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 860575a..935dd96 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -37,9 +37,11 @@
 import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 
 public final class SubtypeSwitcher {
     private static boolean DBG = LatinImeLogger.sDBG;
@@ -273,12 +275,26 @@
         return mNeedsToDisplayLanguage.getValue();
     }
 
-    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypes() {
+    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
         final Locale systemLocale = mResources.getConfiguration().locale;
-        final List<InputMethodSubtype> enabledSubtypesOfThisIme =
-                mRichImm.getMyEnabledInputMethodSubtypeList(true);
-        for (final InputMethodSubtype subtype : enabledSubtypesOfThisIme) {
-            if (!systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
+        final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes =
+                new HashSet<InputMethodSubtype>();
+        final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager();
+        final List<InputMethodInfo> enabledInputMethodInfoList =
+                inputMethodManager.getEnabledInputMethodList();
+        for (final InputMethodInfo info : enabledInputMethodInfoList) {
+            final List<InputMethodSubtype> enabledSubtypes =
+                    inputMethodManager.getEnabledInputMethodSubtypeList(
+                            info, true /* allowsImplicitlySelectedSubtypes */);
+            if (enabledSubtypes.isEmpty()) {
+                // An IME with no subtypes is found.
+                return false;
+            }
+            enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
+        }
+        for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
+            if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
+                    && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
                 return false;
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 3fc2cf8..52a6f5f 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -77,9 +77,10 @@
     public int mSpaceState;
     // Never null
     public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
-    public Suggest mSuggest;
+    // TODO: mSuggest should be touched by a single thread.
+    public volatile Suggest mSuggest;
     // The event interpreter should never be null.
-    public EventInterpreter mEventInterpreter;
+    public final EventInterpreter mEventInterpreter;
 
     public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     public final WordComposer mWordComposer;
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 23aa05d..88fff38 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -53,6 +53,10 @@
 
     @Override
     public DictionaryHeader readHeader() throws IOException, UnsupportedFormatException {
+        final DictionaryHeader header = mBinaryDictionary.getHeader();
+        if (header == null) {
+            throw new IOException("Cannot read the dictionary header.");
+        }
         return mBinaryDictionary.getHeader();
     }
 
diff --git a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
index 7844195..38e8ff1 100644
--- a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
@@ -100,6 +100,8 @@
     // starts iterating the dictionary.
     virtual int getNextWordAndNextToken(const int token, int *const outCodePoints) = 0;
 
+    virtual bool isCorrupted() const = 0;
+
  protected:
     DictionaryStructureWithBufferPolicy() {}
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
index fa59930..212f2ef 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
@@ -36,6 +36,7 @@
     if (nextPos < 0 || nextPos >= mDictBufferSize) {
         AKLOGE("Children PtNode array position is invalid. pos: %d, dict size: %d",
                 nextPos, mDictBufferSize);
+        mIsCorrupted = true;
         ASSERT(false);
         return;
     }
@@ -45,6 +46,7 @@
         if (nextPos < 0 || nextPos >= mDictBufferSize) {
             AKLOGE("Child PtNode position is invalid. pos: %d, dict size: %d, childCount: %d / %d",
                     nextPos, mDictBufferSize, i, childCount);
+            mIsCorrupted = true;
             ASSERT(false);
             return;
         }
@@ -239,7 +241,13 @@
         const int length, const bool forceLowerCaseSearch) const {
     DynamicPtReadingHelper readingHelper(&mPtNodeReader, &mPtNodeArrayReader);
     readingHelper.initWithPtNodeArrayPos(getRootPosition());
-    return readingHelper.getTerminalPtNodePositionOfWord(inWord, length, forceLowerCaseSearch);
+    const int ptNodePos =
+            readingHelper.getTerminalPtNodePositionOfWord(inWord, length, forceLowerCaseSearch);
+    if (readingHelper.isError()) {
+        mIsCorrupted = true;
+        AKLOGE("Dictionary reading error in createAndGetAllChildDicNodes().");
+    }
+    return ptNodePos;
 }
 
 int PatriciaTriePolicy::getProbability(const int unigramProbability,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
index 8fbca26..6a2345a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
@@ -46,7 +46,7 @@
               mBigramListPolicy(mDictRoot), mShortcutListPolicy(mDictRoot),
               mPtNodeReader(mDictRoot, mDictBufferSize, &mBigramListPolicy, &mShortcutListPolicy),
               mPtNodeArrayReader(mDictRoot, mDictBufferSize),
-              mTerminalPtNodePositionsForIteratingWords() {}
+              mTerminalPtNodePositionsForIteratingWords(), mIsCorrupted(false) {}
 
     AK_FORCE_INLINE int getRootPosition() const {
         return 0;
@@ -134,6 +134,10 @@
 
     int getNextWordAndNextToken(const int token, int *const outCodePoints);
 
+    bool isCorrupted() const {
+        return mIsCorrupted;
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(PatriciaTriePolicy);
 
@@ -146,6 +150,7 @@
     const Ver2ParticiaTrieNodeReader mPtNodeReader;
     const Ver2PtNodeArrayReader mPtNodeArrayReader;
     std::vector<int> mTerminalPtNodePositionsForIteratingWords;
+    mutable bool mIsCorrupted;
 
     int createAndGetLeavingChildNode(const DicNode *const dicNode, const int ptNodePos,
             DicNodeVector *const childDicNodes) const;
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 efc29a0..b5d80be 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
@@ -65,6 +65,10 @@
                 ptNodeParams.getCodePointCount(), ptNodeParams.getCodePoints());
         readingHelper.readNextSiblingNode(ptNodeParams);
     }
+    if (readingHelper.isError()) {
+        mIsCorrupted = true;
+        AKLOGE("Dictionary reading error in createAndGetAllChildDicNodes().");
+    }
 }
 
 int Ver4PatriciaTriePolicy::getCodePointsAndProbabilityAndReturnCodePointCount(
@@ -72,15 +76,26 @@
         int *const outUnigramProbability) const {
     DynamicPtReadingHelper readingHelper(&mNodeReader, &mPtNodeArrayReader);
     readingHelper.initWithPtNodePos(ptNodePos);
-    return readingHelper.getCodePointsAndProbabilityAndReturnCodePointCount(
+    const int codePointCount =  readingHelper.getCodePointsAndProbabilityAndReturnCodePointCount(
             maxCodePointCount, outCodePoints, outUnigramProbability);
+    if (readingHelper.isError()) {
+        mIsCorrupted = true;
+        AKLOGE("Dictionary reading error in getCodePointsAndProbabilityAndReturnCodePointCount().");
+    }
+    return codePointCount;
 }
 
 int Ver4PatriciaTriePolicy::getTerminalPtNodePositionOfWord(const int *const inWord,
         const int length, const bool forceLowerCaseSearch) const {
     DynamicPtReadingHelper readingHelper(&mNodeReader, &mPtNodeArrayReader);
     readingHelper.initWithPtNodeArrayPos(getRootPosition());
-    return readingHelper.getTerminalPtNodePositionOfWord(inWord, length, forceLowerCaseSearch);
+    const int ptNodePos =
+            readingHelper.getTerminalPtNodePositionOfWord(inWord, length, forceLowerCaseSearch);
+    if (readingHelper.isError()) {
+        mIsCorrupted = true;
+        AKLOGE("Dictionary reading error in createAndGetAllChildDicNodes().");
+    }
+    return ptNodePos;
 }
 
 int Ver4PatriciaTriePolicy::getProbability(const int unigramProbability,
@@ -265,7 +280,10 @@
         AKLOGI("Warning: flush() is called for non-updatable dictionary. filePath: %s", filePath);
         return;
     }
-    mWritingHelper.writeToDictFile(filePath, mUnigramCount, mBigramCount);
+    if (!mWritingHelper.writeToDictFile(filePath, mUnigramCount, mBigramCount)) {
+        AKLOGE("Cannot flush the dictionary to file.");
+        mIsCorrupted = true;
+    }
 }
 
 void Ver4PatriciaTriePolicy::flushWithGC(const char *const filePath) {
@@ -273,7 +291,10 @@
         AKLOGI("Warning: flushWithGC() is called for non-updatable dictionary.");
         return;
     }
-    mWritingHelper.writeToDictFileWithGC(getRootPosition(), filePath);
+    if (!mWritingHelper.writeToDictFileWithGC(getRootPosition(), filePath)) {
+        AKLOGE("Cannot flush the dictionary to file with GC.");
+        mIsCorrupted = true;
+    }
 }
 
 bool Ver4PatriciaTriePolicy::needsToRunGC(const bool mindsBlockByGC) const {
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 6921630..7796e2d 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
@@ -55,7 +55,7 @@
               mWritingHelper(mBuffers.get()),
               mUnigramCount(mHeaderPolicy->getUnigramCount()),
               mBigramCount(mHeaderPolicy->getBigramCount()),
-              mTerminalPtNodePositionsForIteratingWords() {};
+              mTerminalPtNodePositionsForIteratingWords(), mIsCorrupted(false) {};
 
     AK_FORCE_INLINE int getRootPosition() const {
         return 0;
@@ -116,6 +116,10 @@
 
     int getNextWordAndNextToken(const int token, int *const outCodePoints);
 
+    bool isCorrupted() const {
+        return mIsCorrupted;
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Ver4PatriciaTriePolicy);
 
@@ -141,6 +145,7 @@
     int mUnigramCount;
     int mBigramCount;
     std::vector<int> mTerminalPtNodePositionsForIteratingWords;
+    mutable bool mIsCorrupted;
 };
 } // namespace latinime
 #endif // LATINIME_VER4_PATRICIA_TRIE_POLICY_H
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
index acf0991..93053c3 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
@@ -33,7 +33,7 @@
 
 namespace latinime {
 
-void Ver4PatriciaTrieWritingHelper::writeToDictFile(const char *const dictDirPath,
+bool Ver4PatriciaTrieWritingHelper::writeToDictFile(const char *const dictDirPath,
         const int unigramCount, const int bigramCount) const {
     const HeaderPolicy *const headerPolicy = mBuffers->getHeaderPolicy();
     BufferWithExtendableBuffer headerBuffer(
@@ -46,12 +46,12 @@
                 "updatesLastDecayedTime: %d, unigramCount: %d, bigramCount: %d, "
                 "extendedRegionSize: %d", false, unigramCount, bigramCount,
                 extendedRegionSize);
-        return;
+        return false;
     }
-    mBuffers->flushHeaderAndDictBuffers(dictDirPath, &headerBuffer);
+    return mBuffers->flushHeaderAndDictBuffers(dictDirPath, &headerBuffer);
 }
 
-void Ver4PatriciaTrieWritingHelper::writeToDictFileWithGC(const int rootPtNodeArrayPos,
+bool Ver4PatriciaTrieWritingHelper::writeToDictFileWithGC(const int rootPtNodeArrayPos,
         const char *const dictDirPath) {
     const HeaderPolicy *const headerPolicy = mBuffers->getHeaderPolicy();
     Ver4DictBuffers::Ver4DictBuffersPtr dictBuffers(
@@ -59,15 +59,15 @@
     int unigramCount = 0;
     int bigramCount = 0;
     if (!runGC(rootPtNodeArrayPos, headerPolicy, dictBuffers.get(), &unigramCount, &bigramCount)) {
-        return;
+        return false;
     }
     BufferWithExtendableBuffer headerBuffer(
             BufferWithExtendableBuffer::DEFAULT_MAX_ADDITIONAL_BUFFER_SIZE);
     if (!headerPolicy->fillInAndWriteHeaderToBuffer(true /* updatesLastDecayedTime */,
             unigramCount, bigramCount, 0 /* extendedRegionSize */, &headerBuffer)) {
-        return;
+        return false;
     }
-    dictBuffers.get()->flushHeaderAndDictBuffers(dictDirPath, &headerBuffer);
+    return dictBuffers.get()->flushHeaderAndDictBuffers(dictDirPath, &headerBuffer);
 }
 
 bool Ver4PatriciaTrieWritingHelper::runGC(const int rootPtNodeArrayPos,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.h
index c3a155e..bb464ad 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.h
@@ -33,10 +33,12 @@
     Ver4PatriciaTrieWritingHelper(Ver4DictBuffers *const buffers)
             : mBuffers(buffers) {}
 
-    void writeToDictFile(const char *const dictDirPath, const int unigramCount,
+    bool writeToDictFile(const char *const dictDirPath, const int unigramCount,
             const int bigramCount) const;
 
-    void writeToDictFileWithGC(const int rootPtNodeArrayPos, const char *const dictDirPath);
+    // This method cannot be const because the original dictionary buffer will be updated to detect
+    // useless PtNodes during GC.
+    bool writeToDictFileWithGC(const int rootPtNodeArrayPos, const char *const dictDirPath);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Ver4PatriciaTrieWritingHelper);
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 4b157e7..690c559 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -275,9 +275,9 @@
         }
     }
 
-    protected void waitForDictionaryToBeLoaded() {
+    protected void waitForDictionariesToBeLoaded() {
         try {
-            mLatinIME.waitForMainDictionary(
+            mLatinIME.waitForLoadingDictionaries(
                     TIMEOUT_TO_WAIT_FOR_LOADING_MAIN_DICTIONARY_IN_SECONDS, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
             Log.e(TAG, "Interrupted during waiting for loading main dictionary.", e);
@@ -286,7 +286,7 @@
 
     protected void changeLanguage(final String locale) {
         changeLanguageWithoutWait(locale);
-        waitForDictionaryToBeLoaded();
+        waitForDictionariesToBeLoaded();
     }
 
     protected void changeLanguageWithoutWait(final String locale) {
@@ -314,6 +314,7 @@
         mLatinIME.loadKeyboard();
         runMessages();
         mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
+        mLatinIME.clearPersonalizedDictionariesForTest();
     }
 
     protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale,
@@ -322,7 +323,7 @@
         if (!keyboardLocale.equals(dictLocale)) {
             mLatinIME.replaceDictionariesForTest(LocaleUtils.constructLocaleFromString(dictLocale));
         }
-        waitForDictionaryToBeLoaded();
+        waitForDictionariesToBeLoaded();
     }
 
     protected void pickSuggestionManually(final int index, final String suggestion) {
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index dff5a77..b1239f0 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -20,6 +20,7 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
+import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
@@ -44,6 +45,47 @@
         "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
     };
 
+    private int mCurrentTime = 0;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        resetCurrentTimeForTestMode();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        stopTestModeInNativeCode();
+        super.tearDown();
+    }
+
+    private void resetCurrentTimeForTestMode() {
+        mCurrentTime = 0;
+        setCurrentTimeForTestMode(mCurrentTime);
+    }
+
+    private void forcePassingShortTime() {
+        // 3 days.
+        final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3);
+        mCurrentTime += timeToElapse;
+        setCurrentTimeForTestMode(mCurrentTime);
+    }
+
+    private void forcePassingLongTime() {
+        // 60 days.
+        final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(60);
+        mCurrentTime += timeToElapse;
+        setCurrentTimeForTestMode(mCurrentTime);
+    }
+
+    private static int setCurrentTimeForTestMode(final int currentTime) {
+        return BinaryDictionary.setCurrentTimeForTest(currentTime);
+    }
+
+    private static int stopTestModeInNativeCode() {
+        return BinaryDictionary.setCurrentTimeForTest(-1);
+    }
+
     /**
      * Generates a random word.
      */
@@ -207,4 +249,34 @@
             FileUtils.deleteRecursively(dictFile);
         }
     }
+
+    public void testDecaying() {
+        final Locale dummyLocale = new Locale("test_decaying" + System.currentTimeMillis());
+        final int numberOfWords = 5000;
+        final Random random = new Random(123456);
+        resetCurrentTimeForTestMode();
+        clearHistory(dummyLocale);
+        final List<String> words = generateWords(numberOfWords, random);
+        final UserHistoryDictionary dict =
+                PersonalizationHelper.getUserHistoryDictionary(getContext(), dummyLocale);
+        dict.waitAllTasksForTests();
+        String prevWord = null;
+        for (final String word : words) {
+            dict.addToDictionary(prevWord, word, true, mCurrentTime);
+            prevWord = word;
+            assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
+        }
+        forcePassingShortTime();
+        dict.decayIfNeeded();
+        dict.waitAllTasksForTests();
+        for (final String word : words) {
+            assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
+        }
+        forcePassingLongTime();
+        dict.decayIfNeeded();
+        dict.waitAllTasksForTests();
+        for (final String word : words) {
+            assertFalse(dict.isInUnderlyingBinaryDictionaryForTests(word));
+        }
+    }
 }