Merge "Fix: dicttool build."
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
index 96acb15..ed2db07 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java
@@ -90,7 +90,7 @@
         /*   8:20 */ "more_keys_for_s",
         /*   9:20 */ "more_keys_for_n",
         /*  10:20 */ "label_to_alpha_key",
-        /*  11:15 */ "more_keys_for_y",
+        /*  11:14 */ "more_keys_for_y",
         /*  12:13 */ "more_keys_for_d",
         /*  13:12 */ "more_keys_for_z",
         /*  14:10 */ "more_keys_for_t",
@@ -3585,7 +3585,7 @@
     private static final Object[] LANGUAGES_AND_TEXTS = {
     // "locale", TEXT_ARRAY,  /* numberOfNonNullText/lengthOf_TEXT_ARRAY localeName */
         "DEFAULT", LANGUAGE_DEFAULT, /* 171/171 default */
-        "af", LANGUAGE_af,    /*   8/ 12 Afrikaans */
+        "af", LANGUAGE_af,    /*   7/ 12 Afrikaans */
         "ar", LANGUAGE_ar,    /*  58/110 Arabic */
         "az", LANGUAGE_az_AZ, /*   8/ 17 Azerbaijani (Azerbaijan) */
         "be", LANGUAGE_be_BY, /*  10/ 33 Belarusian (Belarus) */
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
deleted file mode 100644
index 1c6a14e..0000000
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ /dev/null
@@ -1,72 +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;
-
-import android.util.Log;
-
-import com.android.inputmethod.latin.makedict.DictEncoder;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.makedict.Ver4DictEncoder;
-import com.android.inputmethod.latin.utils.FileUtils;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Map;
-
-abstract public class AbstractDictionaryWriter {
-    /** Used for Log actions from this class */
-    private static final String TAG = AbstractDictionaryWriter.class.getSimpleName();
-
-    public AbstractDictionaryWriter() {
-    }
-
-    abstract public void clear();
-
-    /**
-     * Add a unigram with an optional shortcut to the dictionary.
-     * @param word The word to add.
-     * @param shortcutTarget A shortcut target for this word, or null if none.
-     * @param frequency The frequency for this unigram.
-     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
-     *   if shortcutTarget is null.
-     * @param isNotAWord true if this is not a word, i.e. shortcut only.
-     */
-    abstract public void addUnigramWord(final String word, final String shortcutTarget,
-            final int frequency, final int shortcutFreq, final boolean isNotAWord);
-
-    // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve.
-    abstract public void addBigramWords(final String word0, final String word1,
-            final int frequency, final boolean isValid, final long lastModifiedTime);
-
-    abstract public void removeBigramWords(final String word0, final String word1);
-
-    abstract protected void writeDictionary(final DictEncoder dictEncoder,
-            final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException;
-
-    public void write(final File file, final Map<String, String> attributeMap) {
-        try {
-            FileUtils.deleteRecursively(file);
-            file.mkdir();
-            final DictEncoder dictEncoder = new Ver4DictEncoder(file);
-            writeDictionary(dictEncoder, attributeMap);
-        } catch (IOException e) {
-            Log.e(TAG, "IO exception while writing file", e);
-        } catch (UnsupportedFormatException e) {
-            Log.e(TAG, "Unsupported format", e);
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index c450a1d..851ecc0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -28,9 +28,9 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.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.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;
@@ -508,7 +508,9 @@
      */
     @UsedForTesting
     public static int setCurrentTimeForTest(final int currentTime) {
-        return setCurrentTimeForTestNative(currentTime);
+        final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime);
+        PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp);
+        return currentNativeTimestamp;
     }
 
     @UsedForTesting
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index ae9bdf3f..c2941e4 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -78,7 +78,7 @@
     public ContactsBinaryDictionary(final Context context, final Locale locale,
             final File dictFile) {
         super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_CONTACTS,
-                false /* isUpdatable */, dictFile);
+                dictFile);
         mLocale = locale;
         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
         registerObserver(context);
@@ -114,14 +114,14 @@
     }
 
     @Override
-    public void loadDictionaryAsync() {
-        loadDeviceAccountsEmailAddresses();
-        loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
+    public void loadInitialContentsLocked() {
+        loadDeviceAccountsEmailAddressesLocked();
+        loadDictionaryForUriLocked(ContactsContract.Profile.CONTENT_URI);
         // TODO: Switch this URL to the newer ContactsContract too
-        loadDictionaryAsyncForUri(Contacts.CONTENT_URI);
+        loadDictionaryForUriLocked(Contacts.CONTENT_URI);
     }
 
-    private void loadDeviceAccountsEmailAddresses() {
+    private void loadDeviceAccountsEmailAddressesLocked() {
         final List<String> accountVocabulary =
                 AccountUtils.getDeviceAccountsEmailAddresses(mContext);
         if (accountVocabulary == null || accountVocabulary.isEmpty()) {
@@ -131,12 +131,14 @@
             if (DEBUG) {
                 Log.d(TAG, "loadAccountVocabulary: " + word);
             }
-            super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */,
-                    false /* isNotAWord */);
+            runGCIfRequiredLocked(true /* mindsBlockByGC */);
+            addWordDynamicallyLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */,
+                    0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */,
+                    BinaryDictionary.NOT_A_VALID_TIMESTAMP);
         }
     }
 
-    private void loadDictionaryAsyncForUri(final Uri uri) {
+    private void loadDictionaryForUriLocked(final Uri uri) {
         Cursor cursor = null;
         try {
             cursor = mContext.getContentResolver().query(uri, PROJECTION, null, null, null);
@@ -145,7 +147,7 @@
             }
             if (cursor.moveToFirst()) {
                 sContactCountAtLastRebuild = getContactCount();
-                addWords(cursor);
+                addWordsLocked(cursor);
             }
         } catch (final SQLiteException e) {
             Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
@@ -166,12 +168,12 @@
         return false;
     }
 
-    private void addWords(final Cursor cursor) {
+    private void addWordsLocked(final Cursor cursor) {
         int count = 0;
         while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
             String name = cursor.getString(INDEX_NAME);
             if (isValidName(name)) {
-                addName(name);
+                addNameLocked(name);
                 ++count;
             } else {
                 if (DEBUG_DUMP) {
@@ -207,7 +209,7 @@
      * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their
      * bigrams depending on locale.
      */
-    private void addName(final String name) {
+    private void addNameLocked(final String name) {
         int len = StringUtils.codePointCount(name);
         String prevWord = null;
         // TODO: Better tokenization for non-Latin writing systems
@@ -226,13 +228,15 @@
                     if (DEBUG) {
                         Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord);
                     }
-                    super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
-                            0 /* shortcutFreq */, false /* isNotAWord */);
-                    if (!TextUtils.isEmpty(prevWord)) {
-                        if (mUseFirstLastBigrams) {
-                            super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
-                                    0 /* lastModifiedTime */);
-                        }
+                    runGCIfRequiredLocked(true /* mindsBlockByGC */);
+                    addWordDynamicallyLocked(word, FREQUENCY_FOR_CONTACTS,
+                            null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
+                            false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+                    if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
+                        runGCIfRequiredLocked(true /* mindsBlockByGC */);
+                        addBigramDynamicallyLocked(prevWord, word,
+                                FREQUENCY_FOR_CONTACTS_BIGRAM,
+                                BinaryDictionary.NOT_A_VALID_TIMESTAMP);
                     }
                     prevWord = word;
                 }
@@ -258,12 +262,12 @@
     }
 
     @Override
-    protected boolean needsToReloadBeforeWriting() {
+    protected boolean needsToReloadAfterCreation() {
         return true;
     }
 
     @Override
-    protected boolean hasContentChanged() {
+    protected boolean haveContentsChanged() {
         final long startTime = SystemClock.uptimeMillis();
         final int contactCount = getContactCount();
         if (contactCount > MAX_CONTACT_COUNT) {
@@ -291,7 +295,7 @@
             if (cursor.moveToFirst()) {
                 while (!cursor.isAfterLast()) {
                     String name = cursor.getString(INDEX_NAME);
-                    if (isValidName(name) && !isNameInDictionary(name)) {
+                    if (isValidName(name) && !isNameInDictionaryLocked(name)) {
                         if (DEBUG) {
                             Log.d(TAG, "Contact name missing: " + name + " (runtime = "
                                     + (SystemClock.uptimeMillis() - startTime) + " ms)");
@@ -321,7 +325,7 @@
     /**
      * Checks if the words in a name are in the current binary dictionary.
      */
-    private boolean isNameInDictionary(final String name) {
+    private boolean isNameInDictionaryLocked(final String name) {
         int len = StringUtils.codePointCount(name);
         String prevWord = null;
         for (int i = 0; i < len; i++) {
@@ -332,11 +336,11 @@
                 final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
                     if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
-                        if (!super.isValidBigramLocked(prevWord, word)) {
+                        if (!isValidBigramLocked(prevWord, word)) {
                             return false;
                         }
                     } else {
-                        if (!super.isValidWordLocked(word)) {
+                        if (!isValidWordLocked(word)) {
                             return false;
                         }
                     }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
deleted file mode 100644
index b931c66..0000000
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ /dev/null
@@ -1,92 +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;
-
-import com.android.inputmethod.latin.makedict.DictEncoder;
-import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.makedict.FusionDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-import com.android.inputmethod.latin.makedict.ProbabilityInfo;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
-import com.android.inputmethod.latin.utils.CollectionUtils;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * An in memory dictionary for memorizing entries and writing a binary dictionary.
- */
-public class DictionaryWriter extends AbstractDictionaryWriter {
-    private static final int BINARY_DICT_VERSION = FormatSpec.VERSION4;
-    private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
-            new FormatSpec.FormatOptions(BINARY_DICT_VERSION, false /* hasTimestamp */);
-
-    private FusionDictionary mFusionDictionary;
-
-    public DictionaryWriter() {
-        clear();
-    }
-
-    @Override
-    public void clear() {
-        final HashMap<String, String> attributes = CollectionUtils.newHashMap();
-        mFusionDictionary = new FusionDictionary(new PtNodeArray(),
-                new FusionDictionary.DictionaryOptions(attributes));
-    }
-
-    /**
-     * Adds a word unigram to the fusion dictionary.
-     */
-    // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
-    // considering performance regression.
-    @Override
-    public void addUnigramWord(final String word, final String shortcutTarget,
-            final int probability, final int shortcutProbability, final boolean isNotAWord) {
-        if (shortcutTarget == null) {
-            mFusionDictionary.add(word, new ProbabilityInfo(probability), null, isNotAWord);
-        } else {
-            // TODO: Do this in the subclass, with this class taking an arraylist.
-            final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
-            shortcutTargets.add(new WeightedString(shortcutTarget, shortcutProbability));
-            mFusionDictionary.add(word, new ProbabilityInfo(probability), shortcutTargets,
-                    isNotAWord);
-        }
-    }
-
-    @Override
-    public void addBigramWords(final String word0, final String word1, final int probability,
-            final boolean isValid, final long lastModifiedTime) {
-        mFusionDictionary.setBigram(word0, word1, new ProbabilityInfo(probability));
-    }
-
-    @Override
-    public void removeBigramWords(final String word0, final String word1) {
-        // This class don't support removing bigram words.
-    }
-
-    @Override
-    protected void writeDictionary(final DictEncoder dictEncoder,
-            final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException {
-        for (final Map.Entry<String, String> entry : attributeMap.entrySet()) {
-            mFusionDictionary.addOptionAttribute(entry.getKey(), entry.getValue());
-        }
-        dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS);
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c3e33c0..b189515 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -32,7 +32,6 @@
 import com.android.inputmethod.latin.utils.ExecutorUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
-import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -90,10 +89,6 @@
      */
     private BinaryDictionary mBinaryDictionary;
 
-    // TODO: Remove and handle dictionaries in native code.
-    /** The in-memory dictionary used to generate the binary dictionary. */
-    protected AbstractDictionaryWriter mDictionaryWriter;
-
     /**
      * The name of this dictionary, used as a part of the filename for storing the binary
      * dictionary. Multiple dictionary instances with the same name is supported, with access
@@ -104,9 +99,6 @@
     /** Dictionary locale */
     private final Locale mLocale;
 
-    /** Whether to support dynamically updating the dictionary */
-    private final boolean mIsUpdatable;
-
     /** Dictionary file */
     private final File mDictFile;
 
@@ -126,23 +118,22 @@
             new AtomicReference<Runnable>();
 
     /**
-     * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
-     * thread.
+     * Abstract method for loading initial contents of a given dictionary.
      */
-    protected abstract void loadDictionaryAsync();
+    protected abstract void loadInitialContentsLocked();
 
     /**
-     * Indicates that the source dictionary content has changed and a rebuild of the binary file is
-     * required. If it returns false, the next reload will only read the current binary dictionary
-     * from file. Note that the shared binary dictionary is locked when this is called.
+     * Indicates that the source dictionary contents have changed and a rebuild of the binary file
+     * is required. If it returns false, the next reload will only read the current binary
+     * dictionary from file. Note that the shared binary dictionary is locked when this is called.
      */
-    protected abstract boolean hasContentChanged();
+    protected abstract boolean haveContentsChanged();
 
     private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) {
         return formatVersion == FormatSpec.VERSION4;
     }
 
-    public boolean isValidDictionary() {
+    public boolean isValidDictionaryLocked() {
         return mBinaryDictionary.isValidDictionary();
     }
 
@@ -161,15 +152,6 @@
         return recorder;
     }
 
-    private static AbstractDictionaryWriter getDictionaryWriter(
-            final boolean isDynamicPersonalizationDictionary) {
-        if (isDynamicPersonalizationDictionary) {
-             return null;
-        } else {
-            return new DictionaryWriter();
-        }
-    }
-
     /**
      * Creates a new expandable binary dictionary.
      *
@@ -178,24 +160,18 @@
      *        name is supported.
      * @param locale the dictionary locale.
      * @param dictType the dictionary type, as a human-readable string
-     * @param isUpdatable whether to support dynamically updating the dictionary. Please note that
-     *        dynamic dictionary has negative effects on memory space and computation time.
      * @param dictFile dictionary file path. if null, use default dictionary path based on
      *        dictionary type.
      */
     public ExpandableBinaryDictionary(final Context context, final String dictName,
-            final Locale locale, final String dictType, final boolean isUpdatable,
-            final File dictFile) {
+            final Locale locale, final String dictType, final File dictFile) {
         super(dictType);
         mDictName = dictName;
         mContext = context;
         mLocale = locale;
-        mIsUpdatable = isUpdatable;
         mDictFile = getDictFile(context, dictName, dictFile);
         mBinaryDictionary = null;
         mDictNameDictionaryUpdateController = getDictionaryUpdateController(dictName);
-        // Currently, only dynamic personalization dictionary is updatable.
-        mDictionaryWriter = getDictionaryWriter(isUpdatable);
     }
 
     public static File getDictFile(final Context context, final String dictName,
@@ -225,19 +201,6 @@
         });
     }
 
-    protected void closeBinaryDictionary() {
-        // Ensure that no other threads are accessing the local binary dictionary.
-        ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
-            @Override
-            public void run() {
-                if (mBinaryDictionary != null) {
-                    mBinaryDictionary.close();
-                    mBinaryDictionary = null;
-                }
-            }
-        });
-    }
-
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
         attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName);
@@ -257,61 +220,48 @@
         mBinaryDictionary = null;
     }
 
+    private void createBinaryDictionaryLocked() {
+        BinaryDictionary.createEmptyDictFile(mDictFile.getAbsolutePath(),
+                DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap());
+    }
+
+    private void openBinaryDictionaryLocked() {
+        mBinaryDictionary = new BinaryDictionary(
+                mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(),
+                true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */);
+    }
+
     protected void clear() {
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
-                if (mDictionaryWriter == null) {
-                    removeBinaryDictionaryLocked();
-                    BinaryDictionary.createEmptyDictFile(mDictFile.getAbsolutePath(),
-                            DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap());
-                    mBinaryDictionary = new BinaryDictionary(
-                            mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(),
-                            true /* useFullEditDistance */, mLocale, mDictType, mIsUpdatable);
-                } else {
-                    mDictionaryWriter.clear();
-                }
+                removeBinaryDictionaryLocked();
+                createBinaryDictionaryLocked();
+                openBinaryDictionaryLocked();
             }
         });
     }
 
     /**
-     * Adds a word unigram to the dictionary. Used for loading a dictionary.
-     * @param word The word to add.
-     * @param shortcutTarget A shortcut target for this word, or null if none.
-     * @param frequency The frequency for this unigram.
-     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
-     *   if shortcutTarget is null.
-     * @param isNotAWord true if this is not a word, i.e. shortcut only.
-     */
-    protected void addWord(final String word, final String shortcutTarget,
-            final int frequency, final int shortcutFreq, final boolean isNotAWord) {
-        mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, isNotAWord);
-    }
-
-    /**
-     * Adds a word bigram in the dictionary. Used for loading a dictionary.
-     */
-    protected void addBigram(final String prevWord, final String word, final int frequency,
-            final long lastModifiedTime) {
-        mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */,
-                lastModifiedTime);
-    }
-
-    /**
      * Check whether GC is needed and run GC if required.
      */
     protected void runGCIfRequired(final boolean mindsBlockByGC) {
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
-                runGCIfRequiredInternalLocked(mindsBlockByGC);
+                runGCAfterAllPrioritizedTasksIfRequiredLocked(mindsBlockByGC);
             }
         });
     }
 
-    private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) {
-        // Calls to needsToRunGC() need to be serialized.
+    protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) {
+        if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
+            mBinaryDictionary.flushWithGC();
+        }
+    }
+
+    private void runGCAfterAllPrioritizedTasksIfRequiredLocked(final boolean mindsBlockByGC) {
+        // needsToRunGC() have to be called with lock.
         if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
             if (setProcessingLargeTaskIfNot()) {
                 // Run GC after currently existing time sensitive operations.
@@ -335,52 +285,50 @@
     protected void addWordDynamically(final String word, final int frequency,
             final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
             final boolean isBlacklisted, final int timestamp) {
-        if (!mIsUpdatable) {
-            Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mDictName);
-            return;
-        }
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
-                runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
-                mBinaryDictionary.addUnigramWord(word, frequency, shortcutTarget, shortcutFreq,
+                runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */);
+                addWordDynamicallyLocked(word, frequency, shortcutTarget, shortcutFreq,
                         isNotAWord, isBlacklisted, timestamp);
             }
         });
     }
 
+    protected void addWordDynamicallyLocked(final String word, final int frequency,
+            final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord,
+            final boolean isBlacklisted, final int timestamp) {
+        mBinaryDictionary.addUnigramWord(word, frequency, shortcutTarget, shortcutFreq,
+                isNotAWord, isBlacklisted, timestamp);
+    }
+
     /**
      * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
      */
     protected void addBigramDynamically(final String word0, final String word1,
             final int frequency, final int timestamp) {
-        if (!mIsUpdatable) {
-            Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
-                    + mDictName);
-            return;
-        }
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
-                runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
-                mBinaryDictionary.addBigramWords(word0, word1, frequency, timestamp);
+                runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */);
+                addBigramDynamicallyLocked(word0, word1, frequency, timestamp);
             }
         });
     }
 
+    protected void addBigramDynamicallyLocked(final String word0, final String word1,
+            final int frequency, final int timestamp) {
+        mBinaryDictionary.addBigramWords(word0, word1, frequency, timestamp);
+    }
+
     /**
      * Dynamically remove a word bigram in the dictionary.
      */
     protected void removeBigramDynamically(final String word0, final String word1) {
-        if (!mIsUpdatable) {
-            Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: "
-                    + mDictName);
-            return;
-        }
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
-                runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
+                runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */);
                 mBinaryDictionary.removeBigramWords(word0, word1);
             }
         });
@@ -396,11 +344,6 @@
     protected void addMultipleDictionaryEntriesDynamically(
             final ArrayList<LanguageModelParam> languageModelParams,
             final AddMultipleDictionaryEntriesCallback callback) {
-        if (!mIsUpdatable) {
-            Log.w(TAG, "addMultipleDictionaryEntriesDynamically is called for non-updatable " +
-                    "dictionary: " + mDictName);
-            return;
-        }
         ExecutorUtils.getExecutor(mDictName).execute(new Runnable() {
             @Override
             public void run() {
@@ -463,10 +406,6 @@
     @Override
     public boolean isValidWord(final String word) {
         reloadDictionaryIfRequired();
-        return isValidWordInner(word);
-    }
-
-    protected boolean isValidWordInner(final String word) {
         if (processingLargeTask()) {
             return false;
         }
@@ -503,7 +442,7 @@
      * Loads the current binary dictionary from internal storage. Assumes the dictionary file
      * exists.
      */
-    private void loadBinaryDictionary() {
+    private void loadBinaryDictionaryLocked() {
         if (DEBUG) {
             Log.d(TAG, "Loading binary dictionary: " + mDictName + " request="
                     + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
@@ -519,65 +458,40 @@
             } catch (InterruptedException e) {
             }
         }
-
-        final String filename = mDictFile.getAbsolutePath();
-        final long length = mDictFile.length();
-
-        // Build the new binary dictionary
-        final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0 /* offset */,
-                length, true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
-
-        // Ensure all threads accessing the current dictionary have finished before
-        // swapping in the new one.
-        // TODO: Ensure multi-thread assignment of mBinaryDictionary.
         final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
-        ExecutorUtils.getExecutor(mDictName).executePrioritized(new Runnable() {
-            @Override
-            public void run() {
-                mBinaryDictionary = newBinaryDictionary;
-                if (oldBinaryDictionary != null) {
-                    oldBinaryDictionary.close();
-                }
-            }
-        });
+        openBinaryDictionaryLocked();
+        if (oldBinaryDictionary != null) {
+            oldBinaryDictionary.close();
+        }
     }
 
     /**
      * Abstract method for checking if it is required to reload the dictionary before writing
      * a binary dictionary.
      */
-    abstract protected boolean needsToReloadBeforeWriting();
+    abstract protected boolean needsToReloadAfterCreation();
 
     /**
-     * Writes a new binary dictionary based on the contents of the fusion dictionary.
+     * Create a new binary dictionary and load initial contents.
      */
-    private void writeBinaryDictionary() {
+    private void createNewDictionaryLocked() {
         if (DEBUG) {
             Log.d(TAG, "Generating binary dictionary: " + mDictName + " request="
                     + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update="
                     + mDictNameDictionaryUpdateController.mLastUpdateTime);
         }
-        if (needsToReloadBeforeWriting()) {
-            mDictionaryWriter.clear();
-            loadDictionaryAsync();
-            mDictionaryWriter.write(mDictFile, getHeaderAttributeMap());
+        removeBinaryDictionaryLocked();
+        createBinaryDictionaryLocked();
+        openBinaryDictionaryLocked();
+        loadInitialContentsLocked();
+        mBinaryDictionary.flushWithGC();
+    }
+
+    private void flushDictionaryLocked() {
+        if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
+            mBinaryDictionary.flushWithGC();
         } else {
-            if (mBinaryDictionary == null || !isValidDictionary()
-                    // TODO: remove the check below
-                    || !matchesExpectedBinaryDictFormatVersionForThisType(
-                            mBinaryDictionary.getFormatVersion())) {
-                if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) {
-                    Log.e(TAG, "Can't remove a file: " + mDictFile.getName());
-                }
-                BinaryDictionary.createEmptyDictFile(mDictFile.getAbsolutePath(),
-                        DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap());
-            } else {
-                if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
-                    mBinaryDictionary.flushWithGC();
-                } else {
-                    mBinaryDictionary.flush();
-                }
-            }
+            mBinaryDictionary.flush();
         }
     }
 
@@ -638,52 +552,38 @@
             public void run() {
                 try {
                     final long time = System.currentTimeMillis();
-                    final boolean dictionaryFileExists = dictionaryFileExists();
-                    if (mDictNameDictionaryUpdateController.isOutOfDate()
-                            || !dictionaryFileExists) {
-                        // If the shared dictionary file does not exist or is out of date, the
-                        // first instance that acquires the lock will generate a new one.
-                        if (hasContentChanged() || !dictionaryFileExists) {
-                            // If the source content has changed or the dictionary does not exist,
-                            // rebuild the binary dictionary. Empty dictionaries are supported (in
-                            // the case where loadDictionaryAsync() adds nothing) in order to
-                            // provide a uniform framework.
-                            mDictNameDictionaryUpdateController.mLastUpdateTime = time;
-                            writeBinaryDictionary();
-                            loadBinaryDictionary();
-                        } else {
-                            // If not, the reload request was unnecessary so revert
-                            // LastUpdateRequestTime to LastUpdateTime.
-                            mDictNameDictionaryUpdateController.mLastUpdateRequestTime =
-                                    mDictNameDictionaryUpdateController.mLastUpdateTime;
-                        }
+                    final boolean openedDictIsOutOfDate =
+                            mDictNameDictionaryUpdateController.isOutOfDate();
+                    if (!dictionaryFileExists()
+                            || (openedDictIsOutOfDate && haveContentsChanged())) {
+                        // If the shared dictionary file does not exist or is out of date and
+                        // contents have been updated, the first instance that acquires the lock
+                        // will generate a new one
+                        mDictNameDictionaryUpdateController.mLastUpdateTime = time;
+                        createNewDictionaryLocked();
+                    } else if (openedDictIsOutOfDate) {
+                        // If not, the reload request was unnecessary so revert
+                        // LastUpdateRequestTime to LastUpdateTime.
+                        mDictNameDictionaryUpdateController.mLastUpdateRequestTime =
+                                mDictNameDictionaryUpdateController.mLastUpdateTime;
                     } else if (mBinaryDictionary == null ||
                             mPerInstanceDictionaryUpdateController.mLastUpdateTime
                                     < mDictNameDictionaryUpdateController.mLastUpdateTime) {
                         // Otherwise, if the local dictionary is older than the shared dictionary,
                         // load the shared dictionary.
-                        loadBinaryDictionary();
+                        loadBinaryDictionaryLocked();
                     }
-                    // If we just loaded the binary dictionary, then mBinaryDictionary is not
-                    // up-to-date yet so it's useless to test it right away. Schedule the check
-                    // for right after it's loaded instead.
-                    ExecutorUtils.getExecutor(mDictName).executePrioritized(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (mBinaryDictionary != null && !(isValidDictionary()
-                                    // TODO: remove the check below
-                                    && matchesExpectedBinaryDictFormatVersionForThisType(
-                                            mBinaryDictionary.getFormatVersion()))) {
-                                // Binary dictionary or its format version is not valid. Regenerate
-                                // the dictionary file. writeBinaryDictionary will remove the
-                                // existing files if appropriate.
-                                mDictNameDictionaryUpdateController.mLastUpdateTime = time;
-                                writeBinaryDictionary();
-                                loadBinaryDictionary();
-                            }
-                            mPerInstanceDictionaryUpdateController.mLastUpdateTime = time;
-                        }
-                    });
+                    if (mBinaryDictionary != null && !(isValidDictionaryLocked()
+                            // TODO: remove the check below
+                            && matchesExpectedBinaryDictFormatVersionForThisType(
+                                    mBinaryDictionary.getFormatVersion()))) {
+                        // Binary dictionary or its format version is not valid. Regenerate
+                        // the dictionary file. writeBinaryDictionary will remove the
+                        // existing files if appropriate.
+                        mDictNameDictionaryUpdateController.mLastUpdateTime = time;
+                        createNewDictionaryLocked();
+                    }
+                    mPerInstanceDictionaryUpdateController.mLastUpdateTime = time;
                 } finally {
                     mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false);
                 }
@@ -697,13 +597,13 @@
     }
 
     /**
-     * Generate binary dictionary using DictionaryWriter.
+     * Flush binary dictionary to dictionary file.
      */
     protected void asyncFlushBinaryDictionary() {
         final Runnable newTask = new Runnable() {
             @Override
             public void run() {
-                writeBinaryDictionary();
+                flushDictionaryLocked();
             }
         };
         final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask);
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 8ab1bb6..bfc5780 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -172,7 +172,11 @@
         }
 
         public void onCreate() {
-            final Resources res = getOwnerInstance().getResources();
+            final LatinIME latinIme = getOwnerInstance();
+            if (latinIme == null) {
+                return;
+            }
+            final Resources res = latinIme.getResources();
             mDelayUpdateSuggestions = res.getInteger(R.integer.config_delay_update_suggestions);
             mDelayUpdateShiftState = res.getInteger(R.integer.config_delay_update_shift_state);
             mDoubleSpacePeriodTimeout =
@@ -182,6 +186,9 @@
         @Override
         public void handleMessage(final Message msg) {
             final LatinIME latinIme = getOwnerInstance();
+            if (latinIme == null) {
+                return;
+            }
             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
             switch (msg.what) {
             case MSG_UPDATE_SUGGESTION_STRIP:
@@ -239,7 +246,11 @@
         }
 
         public void postResumeSuggestions() {
-            if (!getOwnerInstance().mSettings.getCurrent().isSuggestionStripVisible()) {
+            final LatinIME latinIme = getOwnerInstance();
+            if (latinIme == null) {
+                return;
+            }
+            if (!latinIme.mSettings.getCurrent().isSuggestionStripVisible()) {
                 return;
             }
             removeMessages(MSG_RESUME_SUGGESTIONS);
@@ -326,6 +337,9 @@
             resetPendingImsCallback();
             mIsOrientationChanging = true;
             final LatinIME latinIme = getOwnerInstance();
+            if (latinIme == null) {
+                return;
+            }
             if (latinIme.isInputViewShown()) {
                 latinIme.mKeyboardSwitcher.saveKeyboardState();
             }
@@ -362,8 +376,10 @@
                     mPendingSuccessiveImsCallback = true;
                 }
                 final LatinIME latinIme = getOwnerInstance();
-                executePendingImsCallback(latinIme, editorInfo, restarting);
-                latinIme.onStartInputInternal(editorInfo, restarting);
+                if (latinIme != null) {
+                    executePendingImsCallback(latinIme, editorInfo, restarting);
+                    latinIme.onStartInputInternal(editorInfo, restarting);
+                }
             }
         }
 
@@ -381,9 +397,11 @@
                             PENDING_IMS_CALLBACK_DURATION);
                 }
                 final LatinIME latinIme = getOwnerInstance();
-                executePendingImsCallback(latinIme, editorInfo, restarting);
-                latinIme.onStartInputViewInternal(editorInfo, restarting);
-                mAppliedEditorInfo = editorInfo;
+                if (latinIme != null) {
+                    executePendingImsCallback(latinIme, editorInfo, restarting);
+                    latinIme.onStartInputViewInternal(editorInfo, restarting);
+                    mAppliedEditorInfo = editorInfo;
+                }
             }
         }
 
@@ -393,8 +411,10 @@
                 mHasPendingFinishInputView = true;
             } else {
                 final LatinIME latinIme = getOwnerInstance();
-                latinIme.onFinishInputViewInternal(finishingInput);
-                mAppliedEditorInfo = null;
+                if (latinIme != null) {
+                    latinIme.onFinishInputViewInternal(finishingInput);
+                    mAppliedEditorInfo = null;
+                }
             }
         }
 
@@ -404,8 +424,10 @@
                 mHasPendingFinishInput = true;
             } else {
                 final LatinIME latinIme = getOwnerInstance();
-                executePendingImsCallback(latinIme, null, false);
-                latinIme.onFinishInputInternal();
+                if (latinIme != null) {
+                    executePendingImsCallback(latinIme, null, false);
+                    latinIme.onFinishInputInternal();
+                }
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 3e3cbf0..8078ab5 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -86,8 +86,7 @@
 
     public UserBinaryDictionary(final Context context, final Locale locale,
             final boolean alsoUseMoreRestrictiveLocales, final File dictFile) {
-        super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_USER,
-                false /* isUpdatable */, dictFile);
+        super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile);
         if (null == locale) throw new NullPointerException(); // Catch the error earlier
         final String localeStr = locale.toString();
         if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) {
@@ -130,7 +129,7 @@
     }
 
     @Override
-    public void loadDictionaryAsync() {
+    public void loadInitialContentsLocked() {
         // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
         // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
         // This is correct for locale processing.
@@ -182,7 +181,7 @@
         try {
             cursor = mContext.getContentResolver().query(
                 Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
-            addWords(cursor);
+            addWordsLocked(cursor);
         } catch (final SQLiteException e) {
             Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
         } finally {
@@ -236,7 +235,7 @@
         }
     }
 
-    private void addWords(final Cursor cursor) {
+    private void addWordsLocked(final Cursor cursor) {
         final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
         if (cursor == null) return;
         if (cursor.moveToFirst()) {
@@ -250,12 +249,16 @@
                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
-                    super.addWord(word, null, adjustedFrequency, 0 /* shortcutFreq */,
-                            false /* isNotAWord */);
-                }
-                if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
-                    super.addWord(shortcut, word, adjustedFrequency, USER_DICT_SHORTCUT_FREQUENCY,
-                            true /* isNotAWord */);
+                    runGCIfRequiredLocked(true /* mindsBlockByGC */);
+                    addWordDynamicallyLocked(word, adjustedFrequency, null /* shortcutTarget */,
+                            0 /* shortcutFreq */, false /* isNotAWord */,
+                            false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+                    if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
+                        runGCIfRequiredLocked(true /* mindsBlockByGC */);
+                        addWordDynamicallyLocked(shortcut, adjustedFrequency, word,
+                                USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */,
+                                false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+                    }
                 }
                 cursor.moveToNext();
             }
@@ -263,12 +266,12 @@
     }
 
     @Override
-    protected boolean hasContentChanged() {
+    protected boolean haveContentsChanged() {
         return true;
     }
 
     @Override
-    protected boolean needsToReloadBeforeWriting() {
+    protected boolean needsToReloadAfterCreation() {
         return true;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
index b09e205..db96de3 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java
@@ -139,7 +139,7 @@
                                     forEnd /* dismissGestureFloatingPreviewText */);
                             if (forEnd) {
                                 mInBatchInput = false;
-                                // The following call schedules onEndBatchInputAsyncInternal
+                                // The following call schedules onEndBatchInputInternal
                                 // to be called on the UI thread.
                                 mLatinIME.mHandler.onEndBatchInput(suggestedWords);
                             }
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
index a5dc456..678c5ca 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.makedict;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 
@@ -25,6 +26,7 @@
  * An interface of binary dictionary encoder.
  */
 public interface DictEncoder {
+    @UsedForTesting
     public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
             throws IOException, UnsupportedFormatException;
 
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 8c358cd..074ec40 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -27,10 +27,8 @@
 
 import java.io.File;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
 /**
  * This class is a base class of a dictionary that supports decaying for the personalized language
@@ -49,15 +47,13 @@
     /** The locale for this dictionary. */
     public final Locale mLocale;
 
-    private final String mDictName;
     private Map<String, String> mAdditionalAttributeMap = null;
 
     protected DecayingExpandableBinaryDictionaryBase(final Context context,
             final String dictName, final Locale locale, final String dictionaryType,
             final File dictFile) {
-        super(context, dictName, locale, dictionaryType, true /* isUpdatable */, dictFile);
+        super(context, dictName, locale, dictionaryType, dictFile);
         mLocale = locale;
-        mDictName = dictName;
         if (mLocale != null && mLocale.toString().length() > 1) {
             reloadDictionaryIfRequired();
         }
@@ -79,7 +75,7 @@
 
     @Override
     protected Map<String, String> getHeaderAttributeMap() {
-        final Map<String, String> attributeMap = new HashMap<String, String>();
+        final Map<String, String> attributeMap = super.getHeaderAttributeMap();
         if (mAdditionalAttributeMap != null) {
             attributeMap.putAll(mAdditionalAttributeMap);
         }
@@ -87,20 +83,16 @@
                 DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
         attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY,
                 DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName);
-        attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
-        attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY,
-                String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         return attributeMap;
     }
 
     @Override
-    protected boolean hasContentChanged() {
+    protected boolean haveContentsChanged() {
         return false;
     }
 
     @Override
-    protected boolean needsToReloadBeforeWriting() {
+    protected boolean needsToReloadAfterCreation() {
         return false;
     }
 
@@ -144,8 +136,8 @@
     }
 
     @Override
-    protected void loadDictionaryAsync() {
-        // Never loaded to memory in Java side.
+    protected void loadInitialContentsLocked() {
+        // No initial contents.
     }
 
     @UsedForTesting
@@ -163,7 +155,7 @@
         clearAndFlushDictionary();
     }
 
-    /* package */ void decayIfNeeded() {
+    /* package */ void runGCIfRequired() {
         runGCIfRequired(false /* mindsBlockByGC */);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java
index e9ca662..de2744f 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java
@@ -43,7 +43,7 @@
     /**
      * Interval to update for decaying dictionaries.
      */
-    private static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60);
+    /* package */ static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60);
 
     public static void setUpIntervalAlarmForDictionaryDecaying(Context context) {
         AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
@@ -60,7 +60,7 @@
     public void onReceive(final Context context, final Intent intent) {
         final String action = intent.getAction();
         if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) {
-            PersonalizationHelper.tryDecayingAllOpeningUserHistoryDictionary();
+            PersonalizationHelper.runGCOnAllOpenedUserHistoryDictionaries();
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
index 5ae2fb6..385b525 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.personalization;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 
@@ -27,6 +28,7 @@
 import java.lang.ref.SoftReference;
 import java.util.Locale;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 
 public class PersonalizationHelper {
     private static final String TAG = PersonalizationHelper.class.getSimpleName();
@@ -59,14 +61,35 @@
         }
     }
 
-    public static void tryDecayingAllOpeningUserHistoryDictionary() {
-        for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry
-                : sLangUserHistoryDictCache.entrySet()) {
-            if (entry.getValue() != null) {
-                final UserHistoryDictionary dict = entry.getValue().get();
-                if (dict != null) {
-                    dict.decayIfNeeded();
-                }
+    private static int sCurrentTimestampForTesting = 0;
+    public static void currentTimeChangedForTesting(final int currentTimestamp) {
+        if (TimeUnit.MILLISECONDS.toSeconds(
+                DictionaryDecayBroadcastReciever.DICTIONARY_DECAY_INTERVAL)
+                        < currentTimestamp - sCurrentTimestampForTesting) {
+            // TODO: Run GC for both PersonalizationDictionary and UserHistoryDictionary.
+            runGCOnAllOpenedUserHistoryDictionaries();
+        }
+    }
+
+    public static void runGCOnAllOpenedUserHistoryDictionaries() {
+        runGCOnAllDictionariesIfRequired(sLangUserHistoryDictCache);
+    }
+
+    @UsedForTesting
+    public static void runGCOnAllOpenedPersonalizationDictionaries() {
+        runGCOnAllDictionariesIfRequired(sLangPersonalizationDictCache);
+    }
+
+    private static <T extends DecayingExpandableBinaryDictionaryBase>
+            void runGCOnAllDictionariesIfRequired(
+                    final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap) {
+        for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry
+                : dictionaryMap.entrySet()) {
+            final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get();
+            if (dict != null) {
+                dict.runGCIfRequired();
+            } else {
+                dictionaryMap.remove(entry.getKey());
             }
         }
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
index 6dcf513..9939a43 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
@@ -80,12 +80,25 @@
                 || mScreenMetrics == Constants.SCREEN_METRICS_LARGE_PHONE;
     }
 
+    private static String toString(final ArrayList<InputMethodSubtype> subtypeList) {
+        final StringBuilder sb = new StringBuilder();
+        for (int index = 0; index < subtypeList.size(); index++) {
+            final InputMethodSubtype subtype = subtypeList.get(index);
+            sb.append((index + 1) + ": ");
+            sb.append(SubtypeLocaleUtils.getSubtypeNameForLogging(subtype));
+            sb.append("\n");
+        }
+        return sb.toString();
+    }
+
     public final void testAllSubtypesCount() {
-        assertEquals(NUMBER_OF_SUBTYPES, mAllSubtypesList.size());
+        assertEquals(toString(mAllSubtypesList),
+                NUMBER_OF_SUBTYPES, mAllSubtypesList.size());
     }
 
     public final void testAsciiCapableSubtypesCount() {
-        assertEquals(NUMBER_OF_ASCII_CAPABLE_SUBTYPES, mAsciiCapableSubtypesList.size());
+        assertEquals(toString(mAsciiCapableSubtypesList),
+                NUMBER_OF_ASCII_CAPABLE_SUBTYPES, mAsciiCapableSubtypesList.size());
     }
 
     protected final InputMethodSubtype getSubtype(final Locale locale,
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index b1239f0..6ace2de 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -267,13 +267,13 @@
             assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
         }
         forcePassingShortTime();
-        dict.decayIfNeeded();
+        dict.runGCIfRequired();
         dict.waitAllTasksForTests();
         for (final String word : words) {
             assertTrue(dict.isInUnderlyingBinaryDictionaryForTests(word));
         }
         forcePassingLongTime();
-        dict.decayIfNeeded();
+        dict.runGCIfRequired();
         dict.waitAllTasksForTests();
         for (final String word : words) {
             assertFalse(dict.isInUnderlyingBinaryDictionaryForTests(word));
diff --git a/tools/make-keyboard-text/res/values-af/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-af/donottranslate-more-keys.xml
index ee96f44..5a90e62 100644
--- a/tools/make-keyboard-text/res/values-af/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-af/donottranslate-more-keys.xml
@@ -62,7 +62,6 @@
     <!-- U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
          U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE -->
     <string name="more_keys_for_n">&#x00F1;,&#x0144;</string>
-    <string name="more_keys_for_y">&#x00FD;,&#x0177;,&#x00FF;,&#x0133;</string>
     <!-- U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
          U+0133: "ij" LATIN SMALL LIGATURE IJ -->
     <string name="more_keys_for_y">&#x00FD;,&#x0133;</string>