Merge "Remove redundant check to detect forceAscii"
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 0aa34e8..60ac1ba 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -28,9 +28,10 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.makedict.WordProperty;
-import com.android.inputmethod.latin.personalization.PersonalizationHelper;
 import com.android.inputmethod.latin.settings.NativeSuggestOptions;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
 import com.android.inputmethod.latin.utils.StringUtils;
@@ -81,6 +82,8 @@
     public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2;
     public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3;
 
+    public static final String DICT_FILE_NAME_SUFFIX_FOR_MIGRATION = ".migrate";
+
     private long mNativeDict;
     private final Locale mLocale;
     private final long mDictSize;
@@ -244,7 +247,7 @@
         // TODO: toLowerCase in the native code
         final int[] prevWordCodePointArray = (null == prevWord)
                 ? null : StringUtils.toCodePointArray(prevWord);
-        final int composerSize = composer.size();
+        final int composerSize = composer.sizeWithoutTrailingSingleQuotes();
 
         final boolean isGesture = composer.isBatchMode();
         if (composerSize <= 1 || !isGesture) {
@@ -458,6 +461,24 @@
         return needsToRunGCNative(mNativeDict, mindsBlockByGC);
     }
 
+    public boolean migrateTo(final int newFormatVersion) {
+        if (!isValidDictionary()) {
+            return false;
+        }
+        final String tmpDictFilePath = mDictFilePath + DICT_FILE_NAME_SUFFIX_FOR_MIGRATION;
+        // TODO: Implement migrateNative(tmpDictFilePath, newFormatVersion).
+        close();
+        final File dictFile = new File(mDictFilePath);
+        final File tmpDictFile = new File(tmpDictFilePath);
+        FileUtils.deleteRecursively(dictFile);
+        if (!BinaryDictionaryUtils.renameDict(tmpDictFile, dictFile)) {
+            return false;
+        }
+        loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */,
+                dictFile.length(), mIsUpdatable);
+        return true;
+    }
+
     @UsedForTesting
     public int calculateProbability(final int unigramProbability, final int bigramProbability) {
         if (!isValidDictionary()) return NOT_A_PROBABILITY;
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index c2941e4..4e17f83 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -262,11 +262,6 @@
     }
 
     @Override
-    protected boolean needsToReloadAfterCreation() {
-        return true;
-    }
-
-    @Override
     protected boolean haveContentsChanged() {
         final long startTime = SystemClock.uptimeMillis();
         final int contactCount = getContactCount();
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 3c10159..92b5354 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -137,6 +137,11 @@
         return formatVersion == FormatSpec.VERSION4;
     }
 
+    private boolean needsToMigrateDictionary(final int formatVersion) {
+        // TODO: Check version.
+        return false;
+    }
+
     public boolean isValidDictionaryLocked() {
         return mBinaryDictionary.isValidDictionary();
     }
@@ -477,15 +482,13 @@
         if (oldBinaryDictionary != null) {
             oldBinaryDictionary.close();
         }
+        if (mBinaryDictionary.isValidDictionary()
+                && needsToMigrateDictionary(mBinaryDictionary.getFormatVersion())) {
+            mBinaryDictionary.migrateTo(DICTIONARY_FORMAT_VERSION);
+        }
     }
 
     /**
-     * Abstract method for checking if it is required to reload the dictionary before writing
-     * a binary dictionary.
-     */
-    abstract protected boolean needsToReloadAfterCreation();
-
-    /**
      * Create a new binary dictionary and load initial contents.
      */
     private void createNewDictionaryLocked() {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 6985d9a..db0a8a8 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -101,19 +101,6 @@
                 : typedWord;
         LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED);
 
-        final WordComposer wordComposerForLookup;
-        if (trailingSingleQuotesCount > 0) {
-            wordComposerForLookup = new WordComposer(wordComposer);
-            for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
-                // TODO: do not create a fake event for this. Ideally the word composer should know
-                // how to give out the word without trailing quotes and we can remove this entirely
-                wordComposerForLookup.deleteLast(Event.createSoftwareKeypressEvent(
-                        Event.NOT_A_CODE_POINT, Constants.CODE_DELETE,
-                        Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE));
-            }
-        } else {
-            wordComposerForLookup = wordComposer;
-        }
         final ArrayList<SuggestedWordInfo> rawSuggestions;
         if (ProductionFlag.INCLUDE_RAW_SUGGESTIONS) {
             rawSuggestions = CollectionUtils.newArrayList();
@@ -121,7 +108,7 @@
             rawSuggestions = null;
         }
         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
-                wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords,
+                wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords,
                 additionalFeaturesOptions, SESSION_TYPING, rawSuggestions);
 
         final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 8078ab5..8838e27 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -269,9 +269,4 @@
     protected boolean haveContentsChanged() {
         return true;
     }
-
-    @Override
-    protected boolean needsToReloadAfterCreation() {
-        return true;
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index a955f37..324683c 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -104,25 +104,6 @@
         refreshSize();
     }
 
-    public WordComposer(final WordComposer source) {
-        mCombinerChain = source.mCombinerChain;
-        mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
-        mEvents = new ArrayList<Event>(source.mEvents);
-        mTypedWord = new StringBuilder(source.mTypedWord);
-        mInputPointers.copy(source.mInputPointers);
-        mCapsCount = source.mCapsCount;
-        mDigitsCount = source.mDigitsCount;
-        mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
-        mCapitalizedMode = source.mCapitalizedMode;
-        mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
-        mIsResumed = source.mIsResumed;
-        mIsBatchMode = source.mIsBatchMode;
-        mCursorPositionWithinWord = source.mCursorPositionWithinWord;
-        mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
-        mPreviousWordForSuggestion = source.mPreviousWordForSuggestion;
-        refreshSize();
-    }
-
     /**
      * Clear out the keys registered so far.
      */
@@ -155,6 +136,13 @@
         return mCodePointSize;
     }
 
+    // When the composition contains trailing quotes, we don't pass them to the suggestion engine.
+    // This is because "'tgis'" should be corrected to "'this'", but we can't afford to consider
+    // single quotes as separators because of their very common use as apostrophes.
+    public int sizeWithoutTrailingSingleQuotes() {
+        return size() - mTrailingSingleQuotesCount;
+    }
+
     public final boolean isComposingWord() {
         return size() > 0;
     }
diff --git a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
index 9dcd63f..5fcbb63 100644
--- a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.utils.CombinedFormatUtils;
 
@@ -30,6 +31,7 @@
     public final int mLevel;
     public final int mCount;
 
+    @UsedForTesting
     public static ProbabilityInfo max(final ProbabilityInfo probabilityInfo1,
             final ProbabilityInfo probabilityInfo2) {
         if (probabilityInfo1 == null) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
index d94cec4..8533922 100644
--- a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
+++ b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java
@@ -42,6 +42,7 @@
 
     private int mHashCode = 0;
 
+    @UsedForTesting
     public WordProperty(final String word, final ProbabilityInfo probabilityInfo,
             final ArrayList<WeightedString> shortcutTargets,
             final ArrayList<WeightedString> bigrams,
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 074ec40..6f84e1f 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -91,11 +91,6 @@
         return false;
     }
 
-    @Override
-    protected boolean needsToReloadAfterCreation() {
-        return false;
-    }
-
     public void addMultipleDictionaryEntriesToDictionary(
             final ArrayList<LanguageModelParam> languageModelParams,
             final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
diff --git a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
index 6388300..b4658b5 100644
--- a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
@@ -26,6 +26,8 @@
 import java.io.IOException;
 import java.util.Locale;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public final class BinaryDictionaryUtils {
     private static final String TAG = BinaryDictionaryUtils.class.getSimpleName();
@@ -64,6 +66,31 @@
         return header;
     }
 
+    public static boolean renameDict(final File dictFile, final File newDictFile) {
+        if (dictFile.isFile()) {
+            return dictFile.renameTo(newDictFile);
+        } else if (dictFile.isDirectory()) {
+            final String dictName = dictFile.getName();
+            final String newDictName = newDictFile.getName();
+            if (newDictFile.exists()) {
+                return false;
+            }
+            for (final File file : dictFile.listFiles()) {
+                if (!file.isFile()) {
+                    continue;
+                }
+                final String fileName = file.getName();
+                final String newFileName = fileName.replaceFirst(
+                        Pattern.quote(dictName), Matcher.quoteReplacement(newDictName));
+                if (!file.renameTo(new File(dictFile, newFileName))) {
+                    return false;
+                }
+            }
+            return dictFile.renameTo(newDictFile);
+        }
+        return false;
+    }
+
     public static boolean createEmptyDictFile(final String filePath, final long dictVersion,
             final Locale locale, final Map<String, String> attributeMap) {
         final String[] keyArray = new String[attributeMap.size()];
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 3118cdf..258aa9c 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -83,14 +83,6 @@
 #if DEBUG_DICT
     DicNodeProfiler mProfiler;
 #endif
-    //////////////////
-    // Memory utils //
-    //////////////////
-    AK_FORCE_INLINE static void managedDelete(DicNode *node) {
-        node->remove();
-    }
-    // end
-    /////////////////
 
     AK_FORCE_INLINE DicNode()
             :
@@ -158,7 +150,7 @@
         PROF_NODE_COPY(&dicNode->mProfiler, mProfiler);
     }
 
-    AK_FORCE_INLINE void remove() {
+    AK_FORCE_INLINE void finalize() {
         mIsUsed = false;
         if (mReleaseListener) {
             mReleaseListener->onReleased(this);
@@ -478,17 +470,7 @@
         mReleaseListener = releaseListener;
     }
 
-    AK_FORCE_INLINE bool compare(const DicNode *right) {
-        if (!isUsed() && !right->isUsed()) {
-            // Compare pointer values here for stable comparison
-            return this > right;
-        }
-        if (!isUsed()) {
-            return true;
-        }
-        if (!right->isUsed()) {
-            return false;
-        }
+    AK_FORCE_INLINE bool compare(const DicNode *right) const {
         // Promote exact matches to prevent them from being pruned.
         const bool leftExactMatch = ErrorTypeUtils::isExactMatch(getContainedErrorTypes());
         const bool rightExactMatch = ErrorTypeUtils::isExactMatch(right->getContainedErrorTypes());
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h b/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
index 1f02731..213b1b9 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_priority_queue.h
@@ -68,15 +68,15 @@
         }
         setMaxSize(maxSize);
         for (int i = 0; i < mCapacity + 1; ++i) {
-            mDicNodesBuf[i].remove();
+            mDicNodesBuf[i].finalize();
             mDicNodesBuf[i].setReleaseListener(this);
-            mUnusedNodeIndices[i] = i == mCapacity ? NOT_A_NODE_ID : static_cast<int>(i) + 1;
+            mUnusedNodeIndices[i] = (i == mCapacity) ? NOT_A_NODE_ID : (i + 1);
         }
         mNextUnusedNodeId = 0;
     }
 
     // Copy
-    AK_FORCE_INLINE DicNode *copyPush(DicNode *dicNode) {
+    AK_FORCE_INLINE DicNode *copyPush(const DicNode *const dicNode) {
         return copyPush(dicNode, mMaxSize);
     }
 
@@ -89,11 +89,11 @@
         if (dest) {
             DicNodeUtils::initByCopy(node, dest);
         }
-        node->remove();
+        node->finalize();
         mDicNodesQueue.pop();
     }
 
-    void onReleased(DicNode *dicNode) {
+    void onReleased(const DicNode *dicNode) {
         const int index = static_cast<int>(dicNode - &mDicNodesBuf[0]);
         if (mUnusedNodeIndices[index] != NOT_A_NODE_ID) {
             // it's already released
@@ -118,7 +118,8 @@
     DISALLOW_IMPLICIT_CONSTRUCTORS(DicNodePriorityQueue);
     static const int NOT_A_NODE_ID = -1;
 
-    AK_FORCE_INLINE static bool compareDicNode(DicNode *left, DicNode *right) {
+    AK_FORCE_INLINE static bool compareDicNode(const DicNode *const left,
+            const DicNode *const right) {
         return left->compare(right);
     }
 
@@ -141,10 +142,10 @@
     }
 
     AK_FORCE_INLINE void pop() {
-        copyPop(0);
+        copyPop(nullptr);
     }
 
-    AK_FORCE_INLINE bool betterThanWorstDicNode(DicNode *dicNode) const {
+    AK_FORCE_INLINE bool betterThanWorstDicNode(const DicNode *const dicNode) const {
         DicNode *worstNode = mDicNodesQueue.top();
         if (!worstNode) {
             return true;
@@ -154,7 +155,7 @@
 
     AK_FORCE_INLINE DicNode *searchEmptyDicNode() {
         if (mCapacity == 0) {
-            return 0;
+            return nullptr;
         }
         if (mNextUnusedNodeId == NOT_A_NODE_ID) {
             AKLOGI("No unused node found.");
@@ -163,7 +164,7 @@
                         i, mDicNodesBuf[i].isUsed(), mUnusedNodeIndices[i]);
             }
             ASSERT(false);
-            return 0;
+            return nullptr;
         }
         DicNode *dicNode = &mDicNodesBuf[mNextUnusedNodeId];
         markNodeAsUsed(dicNode);
@@ -179,7 +180,7 @@
 
     AK_FORCE_INLINE DicNode *pushPoolNodeWithMaxSize(DicNode *dicNode, const int maxSize) {
         if (!dicNode) {
-            return 0;
+            return nullptr;
         }
         if (!isFull(maxSize)) {
             mDicNodesQueue.push(dicNode);
@@ -190,16 +191,16 @@
             mDicNodesQueue.push(dicNode);
             return dicNode;
         }
-        dicNode->remove();
-        return 0;
+        dicNode->finalize();
+        return nullptr;
     }
 
     // Copy
-    AK_FORCE_INLINE DicNode *copyPush(DicNode *dicNode, const int maxSize) {
+    AK_FORCE_INLINE DicNode *copyPush(const DicNode *const dicNode, const int maxSize) {
         return pushPoolNodeWithMaxSize(newDicNode(dicNode), maxSize);
     }
 
-    AK_FORCE_INLINE DicNode *newDicNode(DicNode *dicNode) {
+    AK_FORCE_INLINE DicNode *newDicNode(const DicNode *const dicNode) {
         DicNode *newNode = searchEmptyDicNode();
         if (newNode) {
             DicNodeUtils::initByCopy(dicNode, newNode);
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_release_listener.h b/native/jni/src/suggest/core/dicnode/dic_node_release_listener.h
index 2ca4f21..c3f4329 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_release_listener.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_release_listener.h
@@ -27,7 +27,7 @@
  public:
     DicNodeReleaseListener() {}
     virtual ~DicNodeReleaseListener() {}
-    virtual void onReleased(DicNode *dicNode) = 0;
+    virtual void onReleased(const DicNode *dicNode) = 0;
  private:
     DISALLOW_COPY_AND_ASSIGN(DicNodeReleaseListener);
 };
diff --git a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
index d4769e7..6b8dc8c 100644
--- a/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
+++ b/native/jni/src/suggest/core/dicnode/dic_nodes_cache.h
@@ -100,14 +100,7 @@
     }
 
     AK_FORCE_INLINE void copyPushNextActive(DicNode *dicNode) {
-        DicNode *pushedDicNode = mNextActiveDicNodes->copyPush(dicNode);
-        if (!pushedDicNode) {
-            if (dicNode->isCached()) {
-                dicNode->remove();
-            }
-            // We simply drop any dic node that was not cached, ignoring the slim chance
-            // that one of its children represents what the user really wanted.
-        }
+        mNextActiveDicNodes->copyPush(dicNode);
     }
 
     void popTerminal(DicNode *dest) {
diff --git a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
index b40f322..d07f5ca 100644
--- a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
+++ b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
@@ -131,7 +131,6 @@
                              true /* forceCommit */, boostExactMatches) : finalScore;
             outputShortcuts(&shortcutIt, shortcutBaseScore, sameAsTyped, outSuggestionResults);
         }
-        DicNode::managedDelete(terminalDicNode);
     }
     scoringPolicy->getMostProbableString(traverseSession, languageWeight, outSuggestionResults);
 }
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 2ea6452..303182c 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -265,7 +265,6 @@
             traverseSession->getDicTraverseCache()->copyPushNextActive(dicNode);
         }
     }
-    DicNode::managedDelete(dicNode);
 }
 
 void Suggest::processDicNodeAsMatch(DicTraverseSession *traverseSession,
@@ -388,7 +387,6 @@
                 processExpandedDicNode(traverseSession, childDicNode2);
             }
         }
-        DicNode::managedDelete(childDicNodes1[i]);
     }
 }
 
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index d4e6ad8..b366452 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -474,4 +474,43 @@
                 WORD_TO_TYPE.length() * TIMES_TO_TYPE - TIMES_TO_BACKSPACE,
                 mEditText.getText().length());
     }
+
+    public void testManySingleQuotes() {
+        final String WORD_TO_AUTOCORRECT = "i";
+        final String WORD_AUTOCORRECTED = "I";
+        final String QUOTES = "''''''''''''''''''''";
+        final String WORD_TO_TYPE = WORD_TO_AUTOCORRECT + QUOTES + " ";
+        final String EXPECTED_RESULT = WORD_AUTOCORRECTED + QUOTES + " ";
+        type(WORD_TO_TYPE);
+        assertEquals("auto-correct with many trailing single quotes", EXPECTED_RESULT,
+                mEditText.getText().toString());
+    }
+
+    public void testManySingleQuotesOneByOne() {
+        final String WORD_TO_AUTOCORRECT = "i";
+        final String WORD_AUTOCORRECTED = "I";
+        final String QUOTES = "''''''''''''''''''''";
+        final String WORD_TO_TYPE = WORD_TO_AUTOCORRECT + QUOTES + " ";
+        final String EXPECTED_RESULT = WORD_AUTOCORRECTED + QUOTES + " ";
+
+        for (int i = 0; i < WORD_TO_TYPE.length(); ++i) {
+            type(WORD_TO_TYPE.substring(i, i+1));
+            sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+            runMessages();
+        }
+        assertEquals("type many trailing single quotes one by one", EXPECTED_RESULT,
+                mEditText.getText().toString());
+    }
+
+    public void testTypingSingleQuotesOneByOne() {
+        final String WORD_TO_TYPE = "it's ";
+        final String EXPECTED_RESULT = WORD_TO_TYPE;
+        for (int i = 0; i < WORD_TO_TYPE.length(); ++i) {
+            type(WORD_TO_TYPE.substring(i, i+1));
+            sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+            runMessages();
+        }
+        assertEquals("type words letter by letter", EXPECTED_RESULT,
+                mEditText.getText().toString());
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java
new file mode 100644
index 0000000..d866391
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java
@@ -0,0 +1,92 @@
+/*
+ * 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.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.makedict.DictionaryHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@LargeTest
+public class BinaryDictionaryUtilsTests extends AndroidTestCase {
+    private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
+    private static final String TEST_LOCALE = "test";
+
+    private File createEmptyDictionaryAndGetFile(final String dictId,
+            final int formatVersion) throws IOException {
+        if (formatVersion == FormatSpec.VERSION4) {
+            return createEmptyVer4DictionaryAndGetFile(dictId);
+        } else {
+            throw new IOException("Dictionary format version " + formatVersion
+                    + " is not supported.");
+        }
+    }
+
+    private File createEmptyVer4DictionaryAndGetFile(final String dictId) throws IOException {
+        final File file = getDictFile(dictId);
+        FileUtils.deleteRecursively(file);
+        Map<String, String> attributeMap = new HashMap<String, String>();
+        attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, dictId);
+        attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY,
+                String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
+        attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY,
+                DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
+        attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY,
+                DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
+        if (BinaryDictionaryUtils.createEmptyDictFile(file.getAbsolutePath(), FormatSpec.VERSION4,
+                LocaleUtils.constructLocaleFromString(TEST_LOCALE), attributeMap)) {
+            return file;
+        } else {
+            throw new IOException("Empty dictionary " + file.getAbsolutePath()
+                    + " cannot be created.");
+        }
+    }
+
+    private File getDictFile(final String dictId) {
+        return new File(getContext().getCacheDir(), dictId + TEST_DICT_FILE_EXTENSION);
+    }
+
+    public void testRenameDictionary() {
+        final int formatVersion = FormatSpec.VERSION4;
+        File dictFile0 = null;
+        try {
+            dictFile0 = createEmptyDictionaryAndGetFile("MoveFromDictionary", formatVersion);
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        }
+        final File dictFile1 = getDictFile("MoveToDictionary");
+        FileUtils.deleteRecursively(dictFile1);
+        assertTrue(BinaryDictionaryUtils.renameDict(dictFile0, dictFile1));
+        assertFalse(dictFile0.exists());
+        assertTrue(dictFile1.exists());
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile1.getAbsolutePath(),
+                0 /* offset */, dictFile1.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+        assertTrue(binaryDictionary.isValidDictionary());
+        assertTrue(binaryDictionary.getFormatVersion() == formatVersion);
+        binaryDictionary.close();
+    }
+}