Merge "Fix possible SIGSEGV."
diff --git a/dictionaries/fr_wordlist.combined.gz b/dictionaries/fr_wordlist.combined.gz
index 0763b62..1d988d6 100644
--- a/dictionaries/fr_wordlist.combined.gz
+++ b/dictionaries/fr_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/iw_wordlist.combined.gz b/dictionaries/iw_wordlist.combined.gz
new file mode 100644
index 0000000..36b0478
--- /dev/null
+++ b/dictionaries/iw_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pt_BR_wordlist.combined.gz b/dictionaries/pt_BR_wordlist.combined.gz
index 0dd8472..221ea75 100644
--- a/dictionaries/pt_BR_wordlist.combined.gz
+++ b/dictionaries/pt_BR_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pt_PT_wordlist.combined.gz b/dictionaries/pt_PT_wordlist.combined.gz
index 00d50d0..6a041d9 100644
--- a/dictionaries/pt_PT_wordlist.combined.gz
+++ b/dictionaries/pt_PT_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/ru_wordlist.combined.gz b/dictionaries/ru_wordlist.combined.gz
index 1c85d66..572314d 100644
--- a/dictionaries/ru_wordlist.combined.gz
+++ b/dictionaries/ru_wordlist.combined.gz
Binary files differ
diff --git a/java/res/raw/main_fr.dict b/java/res/raw/main_fr.dict
index 31fb2af..10adad0 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/res/raw/main_pt_br.dict b/java/res/raw/main_pt_br.dict
index 557d46e..f9ae9b5 100644
--- a/java/res/raw/main_pt_br.dict
+++ b/java/res/raw/main_pt_br.dict
Binary files differ
diff --git a/java/res/raw/main_ru.dict b/java/res/raw/main_ru.dict
index 86c368e..7dec624 100644
--- a/java/res/raw/main_ru.dict
+++ b/java/res/raw/main_ru.dict
Binary files differ
diff --git a/java/res/values-th/donottranslate.xml b/java/res/values-th/donottranslate.xml
new file mode 100644
index 0000000..aeeebed
--- /dev/null
+++ b/java/res/values-th/donottranslate.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Whether this language uses spaces -->
+    <bool name="current_language_has_spaces">false</bool>
+</resources>
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index e352f08..8983536 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -18,6 +18,8 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- TODO: these settings depend on the language. They should be put either in the dictionary
+         header, or in the subtype maybe? -->
     <!-- Symbols that are suggested between words -->
     <string name="suggested_punctuations">!,?,\\,,:,;,\",(,),\',-,/,@,_</string>
     <!-- Symbols that are normally preceded by a space (used to add an auto-space before these) -->
@@ -29,6 +31,8 @@
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors">\'-</string>
+    <!-- Whether this language uses spaces -->
+    <bool name="current_language_has_spaces">true</bool>
 
     <!--  Always show the suggestion strip -->
     <string name="prefs_suggestion_visibility_show_value">0</string>
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 8098dab..143c6e8 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -234,7 +234,7 @@
     public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
             final XmlPullParser parser) throws XmlPullParserException {
         final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
-        final int rowHeight = row.mRowHeight;
+        final int rowHeight = row.getRowHeight();
         mHeight = rowHeight - params.mVerticalGap;
 
         final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
@@ -269,11 +269,11 @@
         final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyIconPreview));
 
-        mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
+        mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
                 | row.getDefaultKeyLabelFlags();
         final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId);
         final Locale locale = params.mId.mLocale;
-        int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+        int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
         String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
 
         int moreKeysColumn = style.getInt(keyAttr,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index f650569..e6a6743 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -24,7 +24,7 @@
     public abstract String[] getStringArray(TypedArray a, int index);
     public abstract String getString(TypedArray a, int index);
     public abstract int getInt(TypedArray a, int index, int defaultValue);
-    public abstract int getFlag(TypedArray a, int index);
+    public abstract int getFlags(TypedArray a, int index);
 
     protected KeyStyle(final KeyboardTextsSet textsSet) {
         mTextsSet = textsSet;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
index 6aab3e7..b21ea3f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
@@ -66,7 +66,7 @@
         }
 
         @Override
-        public int getFlag(final TypedArray a, final int index) {
+        public int getFlags(final TypedArray a, final int index) {
             return a.getInt(index, 0);
         }
     }
@@ -123,14 +123,11 @@
         }
 
         @Override
-        public int getFlag(final TypedArray a, final int index) {
-            int flags = a.getInt(index, 0);
-            final Object value = mStyleAttributes.get(index);
-            if (value != null) {
-                flags |= (Integer)value;
-            }
+        public int getFlags(final TypedArray a, final int index) {
+            final Integer value = (Integer)mStyleAttributes.get(index);
+            final int flags = a.getInt(index, (value != null) ? value : 0);
             final KeyStyle parentStyle = mStyles.get(mParentStyleName);
-            return flags | parentStyle.getFlag(a, index);
+            return flags | parentStyle.getFlags(a, index);
         }
 
         public void readKeyAttributes(final TypedArray keyAttr) {
@@ -142,13 +139,13 @@
             readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
             readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
             readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
-            readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
+            readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
             readString(keyAttr, R.styleable.Keyboard_Key_keyIcon);
             readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled);
             readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview);
             readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn);
             readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
-            readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+            readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
         }
 
         private void readString(final TypedArray a, final int index) {
@@ -163,7 +160,7 @@
             }
         }
 
-        private void readFlag(final TypedArray a, final int index) {
+        private void readFlags(final TypedArray a, final int index) {
             if (a.hasValue(index)) {
                 final Integer value = (Integer)mStyleAttributes.get(index);
                 mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 9bc52e5..3f0773e 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -440,9 +440,6 @@
                 attr, R.styleable.Keyboard_Include);
         final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
         int keyboardLayout = 0;
-        float savedDefaultKeyWidth = 0;
-        int savedDefaultKeyLabelFlags = 0;
-        int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
         try {
             XmlParseUtils.checkAttributeExists(
                     keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
@@ -450,24 +447,10 @@
             keyboardLayout = keyboardAttr.getResourceId(
                     R.styleable.Keyboard_Include_keyboardLayout, 0);
             if (row != null) {
-                if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
-                    // Override current x coordinate.
-                    row.setXPos(row.getKeyX(keyAttr));
-                }
-                // TODO: Remove this if-clause and do the same as backgroundType below.
-                savedDefaultKeyWidth = row.getDefaultKeyWidth();
-                if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
-                    // Override default key width.
-                    row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
-                }
-                savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
-                // Bitwise-or default keyLabelFlag if exists.
-                row.setDefaultKeyLabelFlags(keyAttr.getInt(
-                        R.styleable.Keyboard_Key_keyLabelFlags, 0) | savedDefaultKeyLabelFlags);
-                savedDefaultBackgroundType = row.getDefaultBackgroundType();
-                // Override default backgroundType if exists.
-                row.setDefaultBackgroundType(keyAttr.getInt(
-                        R.styleable.Keyboard_Key_backgroundType, savedDefaultBackgroundType));
+                // Override current x coordinate.
+                row.setXPos(row.getKeyX(keyAttr));
+                // Push current Row attributes and update with new attributes.
+                row.pushRowAttributes(keyAttr);
             }
         } finally {
             keyboardAttr.recycle();
@@ -484,10 +467,8 @@
             parseMerge(parserForInclude, row, skip);
         } finally {
             if (row != null) {
-                // Restore default keyWidth, keyLabelFlags, and backgroundType.
-                row.setDefaultKeyWidth(savedDefaultKeyWidth);
-                row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
-                row.setDefaultBackgroundType(savedDefaultBackgroundType);
+                // Restore Row attributes.
+                row.popRowAttributes();
             }
             parserForInclude.close();
         }
@@ -745,7 +726,7 @@
             mRightEdgeKey = null;
         }
         addEdgeSpace(mParams.mRightPadding, row);
-        mCurrentY += row.mRowHeight;
+        mCurrentY += row.getRowHeight();
         mCurrentRow = null;
         mTopEdge = false;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
index edfcec7..0f9497c 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -23,10 +23,13 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 
+import java.util.ArrayDeque;
+
 /**
  * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
  * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
@@ -38,64 +41,100 @@
     private static final int KEYWIDTH_FILL_RIGHT = -1;
 
     private final KeyboardParams mParams;
-    /** Default width of a key in this row. */
-    private float mDefaultKeyWidth;
-    /** Default height of a key in this row. */
-    public final int mRowHeight;
-    /** Default keyLabelFlags in this row. */
-    private int mDefaultKeyLabelFlags;
-    /** Default backgroundType for this row */
-    private int mDefaultBackgroundType;
+    /** The height of this row. */
+    private final int mRowHeight;
+
+    private final ArrayDeque<RowAttributes> mRowAttributesStack = CollectionUtils.newArrayDeque();
+
+    private static class RowAttributes {
+        /** Default width of a key in this row. */
+        public final float mDefaultKeyWidth;
+        /** Default keyLabelFlags in this row. */
+        public final int mDefaultKeyLabelFlags;
+        /** Default backgroundType for this row */
+        public final int mDefaultBackgroundType;
+
+        /**
+         * Parse and create key attributes. This constructor is used to parse Row tag.
+         *
+         * @param keyAttr an attributes array of Row tag.
+         * @param defaultKeyWidth a default key width.
+         * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+         */
+        public RowAttributes(final TypedArray keyAttr, final float defaultKeyWidth,
+                final int keyboardWidth) {
+            mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+                    keyboardWidth, keyboardWidth, defaultKeyWidth);
+            mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
+            mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+                    Key.BACKGROUND_TYPE_NORMAL);
+        }
+
+        /**
+         * Parse and update key attributes using default attributes. This constructor is used
+         * to parse include tag.
+         *
+         * @param keyAttr an attributes array of include tag.
+         * @param defaultRowAttr default Row attributes.
+         * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+         */
+        public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr,
+                final int keyboardWidth) {
+            mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+                    keyboardWidth, keyboardWidth, defaultRowAttr.mDefaultKeyWidth);
+            mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0)
+                    | defaultRowAttr.mDefaultKeyLabelFlags;
+            mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+                    defaultRowAttr.mDefaultBackgroundType);
+        }
+    }
 
     private final int mCurrentY;
     // Will be updated by {@link Key}'s constructor.
     private float mCurrentX;
 
-    public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
-            final int y) {
+    public KeyboardRow(final Resources res, final KeyboardParams params,
+            final XmlPullParser parser, final int y) {
         mParams = params;
         final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard);
         mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                R.styleable.Keyboard_rowHeight,
-                params.mBaseHeight, params.mDefaultRowHeight);
+                R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
         keyboardAttr.recycle();
         final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard_Key);
-        mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
-                params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth);
-        mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
-                Key.BACKGROUND_TYPE_NORMAL);
+        mRowAttributesStack.push(new RowAttributes(
+                keyAttr, params.mDefaultKeyWidth, params.mBaseWidth));
         keyAttr.recycle();
 
-        // TODO: Initialize this with <Row> attribute as backgroundType is done.
-        mDefaultKeyLabelFlags = 0;
         mCurrentY = y;
         mCurrentX = 0.0f;
     }
 
-    public float getDefaultKeyWidth() {
-        return mDefaultKeyWidth;
+    public int getRowHeight() {
+        return mRowHeight;
     }
 
-    public void setDefaultKeyWidth(final float defaultKeyWidth) {
-        mDefaultKeyWidth = defaultKeyWidth;
+    public void pushRowAttributes(final TypedArray keyAttr) {
+        final RowAttributes newAttributes = new RowAttributes(
+                keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth);
+        mRowAttributesStack.push(newAttributes);
+    }
+
+    public void popRowAttributes() {
+        mRowAttributesStack.pop();
+    }
+
+    public float getDefaultKeyWidth() {
+        return mRowAttributesStack.peek().mDefaultKeyWidth;
     }
 
     public int getDefaultKeyLabelFlags() {
-        return mDefaultKeyLabelFlags;
-    }
-
-    public void setDefaultKeyLabelFlags(final int keyLabelFlags) {
-        mDefaultKeyLabelFlags = keyLabelFlags;
+        return mRowAttributesStack.peek().mDefaultKeyLabelFlags;
     }
 
     public int getDefaultBackgroundType() {
-        return mDefaultBackgroundType;
-    }
-
-    public void setDefaultBackgroundType(final int backgroundType) {
-        mDefaultBackgroundType = backgroundType;
+        return mRowAttributesStack.peek().mDefaultBackgroundType;
     }
 
     public void setXPos(final float keyXPos) {
@@ -128,13 +167,9 @@
         return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
     }
 
-    public float getKeyWidth(final TypedArray keyAttr) {
-        return getKeyWidth(keyAttr, mCurrentX);
-    }
-
     public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) {
         if (keyAttr == null) {
-            return mDefaultKeyWidth;
+            return getDefaultKeyWidth();
         }
         final int widthType = ResourceUtils.getEnumValue(keyAttr,
                 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
@@ -146,7 +181,7 @@
             return keyboardRightEdge - keyXPos;
         default: // KEYWIDTH_NOT_ENUM
             return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
-                    mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth);
+                    mParams.mBaseWidth, mParams.mBaseWidth, getDefaultKeyWidth());
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
index ebbcedc..269b3a2 100644
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -42,8 +42,10 @@
     abstract public void addUnigramWord(final String word, final String shortcutTarget,
             final int frequency, 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 int frequency, final boolean isValid,
+            final long lastModifiedTime);
 
     abstract public void removeBigramWords(final String word0, final String word1);
 
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index c99d0e2..67eb7f3 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -70,7 +70,8 @@
     private final boolean mUseFirstLastBigrams;
 
     public ContactsBinaryDictionary(final Context context, final Locale locale) {
-        super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS);
+        super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS,
+                false /* isUpdatable */);
         mLocale = locale;
         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
         registerObserver(context);
@@ -208,7 +209,8 @@
                             false /* isNotAWord */);
                     if (!TextUtils.isEmpty(prevWord)) {
                         if (mUseFirstLastBigrams) {
-                            super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
+                            super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
+                                    0 /* lastModifiedTime */);
                         }
                     }
                     prevWord = word;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index 1765ce5..1ececd5 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -75,7 +75,7 @@
 
     @Override
     public void addBigramWords(final String word0, final String word1, final int frequency,
-            final boolean isValid) {
+            final boolean isValid, final long lastModifiedTime) {
         mFusionDictionary.setBigram(word0, word1, frequency);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 3f11391..3725677 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -20,6 +20,7 @@
 import android.os.SystemClock;
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.utils.CollectionUtils;
@@ -78,12 +79,18 @@
      */
     private final String mFilename;
 
+    /** Whether to support dynamically updating the dictionary */
+    private final boolean mIsUpdatable;
+
     /** Controls access to the shared binary dictionary file across multiple instances. */
     private final DictionaryController mSharedDictionaryController;
 
     /** Controls access to the local binary dictionary for this instance. */
     private final DictionaryController mLocalDictionaryController = new DictionaryController();
 
+    /* A extension for a binary dictionary file. */
+    public static final String DICT_FILE_EXTENSION = ".dict";
+
     /**
      * Abstract method for loading the unigrams and bigrams of a given dictionary in a background
      * thread.
@@ -110,6 +117,16 @@
         return controller;
     }
 
+    private static AbstractDictionaryWriter getDictionaryWriter(final Context context,
+            final String dictType, final boolean isUpdatable) {
+        if (isUpdatable) {
+            // TODO: Employ dynamically updatable DictionaryWriter.
+            return new DictionaryWriter(context, dictType);
+        } else {
+            return new DictionaryWriter(context, dictType);
+        }
+    }
+
     /**
      * Creates a new expandable binary dictionary.
      *
@@ -117,19 +134,22 @@
      * @param filename The filename for this binary dictionary. Multiple dictionaries with the same
      *        filename is supported.
      * @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.
      */
-    public ExpandableBinaryDictionary(
-            final Context context, final String filename, final String dictType) {
+    public ExpandableBinaryDictionary(final Context context, final String filename,
+            final String dictType, final boolean isUpdatable) {
         super(dictType);
         mFilename = filename;
         mContext = context;
+        mIsUpdatable = isUpdatable;
         mBinaryDictionary = null;
         mSharedDictionaryController = getSharedDictionaryController(filename);
-        mDictionaryWriter = new DictionaryWriter(context, dictType);
+        mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable);
     }
 
     protected static String getFilenameWithLocale(final String name, final String localeStr) {
-        return name + "." + localeStr + ".dict";
+        return name + "." + localeStr + DICT_FILE_EXTENSION;
     }
 
     /**
@@ -137,6 +157,16 @@
      */
     @Override
     public void close() {
+        closeBinaryDictionary();
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            mDictionaryWriter.close();
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
+        }
+    }
+
+    protected void closeBinaryDictionary() {
         // Ensure that no other threads are accessing the local binary dictionary.
         mLocalDictionaryController.writeLock().lock();
         try {
@@ -144,7 +174,6 @@
                 mBinaryDictionary.close();
                 mBinaryDictionary = null;
             }
-            mDictionaryWriter.close();
         } finally {
             mLocalDictionaryController.writeLock().unlock();
         }
@@ -159,35 +188,70 @@
     }
 
     /**
-     * Sets a word bigram in the dictionary. Used for loading a dictionary.
+     * Adds a word bigram in the dictionary. Used for loading a dictionary.
      */
-    protected void setBigram(final String prevWord, final String word, final int frequency) {
-        mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
+    protected void addBigram(final String prevWord, final String word, final int frequency,
+            final long lastModifiedTime) {
+        mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */,
+                lastModifiedTime);
     }
 
     /**
-     * Dynamically adds a word unigram to the dictionary.
+     * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry.
      */
     protected void addWordDynamically(final String word, final String shortcutTarget,
             final int frequency, final boolean isNotAWord) {
-        mLocalDictionaryController.writeLock().lock();
-        try {
-            mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
-        } finally {
-            mLocalDictionaryController.writeLock().unlock();
+        if (!mIsUpdatable) {
+            Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
+            return;
+        }
+        // TODO: Use a queue to reflect what needs to be reflected.
+        if (mLocalDictionaryController.writeLock().tryLock()) {
+            try {
+                mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+            } finally {
+                mLocalDictionaryController.writeLock().unlock();
+            }
         }
     }
 
     /**
-     * Dynamically sets a word bigram in the dictionary.
+     * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
      */
-    protected void setBigramDynamically(final String prevWord, final String word,
-            final int frequency) {
-        mLocalDictionaryController.writeLock().lock();
-        try {
-            mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */);
-        } finally {
-            mLocalDictionaryController.writeLock().unlock();
+    protected void addBigramDynamically(final String word0, final String word1,
+            final int frequency, final boolean isValid) {
+        if (!mIsUpdatable) {
+            Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: "
+                    + mFilename);
+            return;
+        }
+        // TODO: Use a queue to reflect what needs to be reflected.
+        if (mLocalDictionaryController.writeLock().tryLock()) {
+            try {
+                mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid,
+                        0 /* lastTouchedTime */);
+            } finally {
+                mLocalDictionaryController.writeLock().unlock();
+            }
+        }
+    }
+
+    /**
+     * 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: "
+                    + mFilename);
+            return;
+        }
+        // TODO: Use a queue to reflect what needs to be reflected.
+        if (mLocalDictionaryController.writeLock().tryLock()) {
+            try {
+                mDictionaryWriter.removeBigramWords(word0, word1);
+            } finally {
+                mLocalDictionaryController.writeLock().unlock();
+            }
         }
     }
 
@@ -277,7 +341,7 @@
 
         // Build the new binary dictionary
         final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
-                true /* useFullEditDistance */, null, mDictType, false /* isUpdatable */);
+                true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
 
         if (mBinaryDictionary != null) {
             // Ensure all threads accessing the current dictionary have finished before swapping in
@@ -302,9 +366,9 @@
     abstract protected boolean needsToReloadBeforeWriting();
 
     /**
-     * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
+     * Writes a new binary dictionary based on the contents of the fusion dictionary.
      */
-    private void generateBinaryDictionary() {
+    private void writeBinaryDictionary() {
         if (DEBUG) {
             Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
                     + mSharedDictionaryController.mLastUpdateRequestTime + " update="
@@ -367,41 +431,47 @@
     private final void syncReloadDictionaryInternal() {
         // Ensure that only one thread attempts to read or write to the shared binary dictionary
         // file at the same time.
-        mSharedDictionaryController.writeLock().lock();
+        mLocalDictionaryController.writeLock().lock();
         try {
-            final long time = SystemClock.uptimeMillis();
-            final boolean dictionaryFileExists = dictionaryFileExists();
-            if (mSharedDictionaryController.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.
-                    mSharedDictionaryController.mLastUpdateTime = time;
-                    generateBinaryDictionary();
+            mSharedDictionaryController.writeLock().lock();
+            try {
+                final long time = SystemClock.uptimeMillis();
+                final boolean dictionaryFileExists = dictionaryFileExists();
+                if (mSharedDictionaryController.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.
+                        mSharedDictionaryController.mLastUpdateTime = time;
+                        writeBinaryDictionary();
+                        loadBinaryDictionary();
+                    } else {
+                        // If not, the reload request was unnecessary so revert
+                        // LastUpdateRequestTime to LastUpdateTime.
+                        mSharedDictionaryController.mLastUpdateRequestTime =
+                                mSharedDictionaryController.mLastUpdateTime;
+                    }
+                } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
+                        < mSharedDictionaryController.mLastUpdateTime) {
+                    // Otherwise, if the local dictionary is older than the shared dictionary, load
+                    // the shared dictionary.
                     loadBinaryDictionary();
-                } else {
-                    // If not, the reload request was unnecessary so revert LastUpdateRequestTime
-                    // to LastUpdateTime.
-                    mSharedDictionaryController.mLastUpdateRequestTime =
-                            mSharedDictionaryController.mLastUpdateTime;
                 }
-            } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
-                    < mSharedDictionaryController.mLastUpdateTime) {
-                // Otherwise, if the local dictionary is older than the shared dictionary, load the
-                // shared dictionary.
-                loadBinaryDictionary();
+                if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
+                    // Binary dictionary is not valid. Regenerate the dictionary file.
+                    mSharedDictionaryController.mLastUpdateTime = time;
+                    writeBinaryDictionary();
+                    loadBinaryDictionary();
+                }
+                mLocalDictionaryController.mLastUpdateTime = time;
+            } finally {
+                mSharedDictionaryController.writeLock().unlock();
             }
-            if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
-                // Binary dictionary is not valid. Regenerate the dictionary file.
-                mSharedDictionaryController.mLastUpdateTime = time;
-                generateBinaryDictionary();
-                loadBinaryDictionary();
-            }
-            mLocalDictionaryController.mLastUpdateTime = time;
         } finally {
-            mSharedDictionaryController.writeLock().unlock();
+            mLocalDictionaryController.writeLock().unlock();
         }
     }
 
@@ -434,4 +504,45 @@
             return (mLastUpdateRequestTime > mLastUpdateTime);
         }
     }
+
+    /**
+     * Dynamically adds a word unigram to the dictionary for testing with blocking-lock.
+     */
+    @UsedForTesting
+    protected void addWordDynamicallyForTests(final String word, final String shortcutTarget,
+            final int frequency, final boolean isNotAWord) {
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            addWordDynamically(word, shortcutTarget, frequency, isNotAWord);
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Dynamically adds a word bigram in the dictionary for testing with blocking-lock.
+     */
+    @UsedForTesting
+    protected void addBigramDynamicallyForTests(final String word0, final String word1,
+            final int frequency, final boolean isValid) {
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            addBigramDynamically(word0, word1, frequency, isValid);
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Dynamically remove a word bigram in the dictionary for testing with blocking-lock.
+     */
+    @UsedForTesting
+    protected void removeBigramDynamicallyForTests(final String word0, final String word1) {
+        mLocalDictionaryController.writeLock().lock();
+        try {
+            removeBigramDynamically(word0, word1);
+        } finally {
+            mLocalDictionaryController.writeLock().unlock();
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index ffe3171..ee7478c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1948,7 +1948,8 @@
                     }
                 }
             }
-            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
+                    && currentSettings.mCurrentLanguageHasSpaces) {
                 restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
             }
             // We just removed a character. We need to update the auto-caps state.
@@ -1977,6 +1978,9 @@
 
     private void handleCharacter(final int primaryCode, final int x,
             final int y, final int spaceState) {
+        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
+        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
+        // which has the same name as other handle* methods but is not the same.
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
@@ -1996,12 +2000,20 @@
             resetEntireInputState(mLastSelectionStart);
             isComposingWord = false;
         }
-        // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
-        // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
-        // thread here.
-        if (!isComposingWord && currentSettings.isWordCodePoint(primaryCode)
+        // We want to find out whether to start composing a new word with this character. If so,
+        // we need to reset the composing state and switch isComposingWord. The order of the
+        // tests is important for good performance.
+        // We only start composing if we're not already composing.
+        if (!isComposingWord
+        // We only start composing if this is a word code point. Essentially that means it's a
+        // a letter or a word connector.
+                && currentSettings.isWordCodePoint(primaryCode)
+        // We never go into composing state if suggestions are not requested.
                 && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
-                !mConnection.isCursorTouchingWord(currentSettings)) {
+        // In languages with spaces, we only start composing a word when we are not already
+        // touching a word. In languages without spaces, the above conditions are sufficient.
+                (!mConnection.isCursorTouchingWord(currentSettings)
+                        || !currentSettings.mCurrentLanguageHasSpaces)) {
             // Reset entirely the composing state anyway, then start composing a new word unless
             // the character is a single quote. The idea here is, single quote is not a
             // separator and it should be treated as a normal character, except in the first
@@ -2089,16 +2101,20 @@
     private boolean handleSeparator(final int primaryCode, final int x, final int y,
             final int spaceState) {
         boolean didAutoCorrect = false;
+        final SettingsValues currentSettings = mSettings.getCurrent();
+        // We avoid sending spaces in languages without spaces if we were composing.
+        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
+                && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
             // first so that we can insert the separator at the current cursor position.
             resetEntireInputState(mLastSelectionStart);
         }
-        final SettingsValues currentSettings = mSettings.getCurrent();
-        if (mWordComposer.isComposingWord()) {
+        if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
             if (currentSettings.mCorrectionEnabled) {
-                // TODO: maybe cache Strings in an <String> sparse array or something
-                commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
+                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
+                        : new String(new int[] { primaryCode }, 0, 1);
+                commitCurrentAutoCorrection(separator);
                 didAutoCorrect = true;
             } else {
                 commitTyped(new String(new int[]{primaryCode}, 0, 1));
@@ -2115,7 +2131,10 @@
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
         }
-        sendKeyCodePoint(primaryCode);
+
+        if (!shouldAvoidSendingCode) {
+            sendKeyCodePoint(primaryCode);
+        }
 
         if (Constants.CODE_SPACE == primaryCode) {
             if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
@@ -2260,11 +2279,17 @@
         // Get the word on which we should search the bigrams. If we are composing a word, it's
         // whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
         // should just skip whitespace if any, so 1.
-        // TODO: this is slow (2-way IPC) - we should probably cache this instead.
         final SettingsValues currentSettings = mSettings.getCurrent();
-        final String prevWord =
-                mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
-                mWordComposer.isComposingWord() ? 2 : 1);
+        final String prevWord;
+        if (currentSettings.mCurrentLanguageHasSpaces) {
+            // If we are typing in a language with spaces we can just look up the previous
+            // word from textview.
+            prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators,
+                    mWordComposer.isComposingWord() ? 2 : 1);
+        } else {
+            prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
+                    : mLastComposedWord.mCommittedWord;
+        }
         return suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive,
                 currentSettings.mCorrectionEnabled, sessionId);
@@ -2534,6 +2559,9 @@
         // recorrection. This is a temporary, stopgap measure that will be removed later.
         // TODO: remove this.
         if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return;
+        // Recorrection is not supported in languages without spaces because we don't know
+        // how to segment them yet.
+        if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
         // If the cursor is not touching a word, or if there is a selection, return right away.
         if (mLastSelectionStart != mLastSelectionEnd) return;
         // If we don't know the cursor location, return.
@@ -2656,7 +2684,18 @@
         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
             mUserHistoryPredictionDictionary.cancelAddingUserHistory(previousWord, committedWord);
         }
-        mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
+        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+        if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
+            // For languages with spaces, we revert to the typed string, but the cursor is still
+            // after the separator so we don't resume suggestions. If the user wants to correct
+            // the word, they have to press backspace again.
+            mConnection.commitText(stringToCommit, 1);
+        } else {
+            // For languages without spaces, we revert the typed string but the cursor is flush
+            // with the typed word, so we need to resume suggestions right away.
+            mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
+            mConnection.setComposingText(stringToCommit, 1);
+        }
         if (mSettings.isInternal()) {
             LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
@@ -2674,7 +2713,9 @@
 
     // This essentially inserts a space, and that's it.
     public void promotePhantomSpace() {
-        if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
+        final SettingsValues currentSettings = mSettings.getCurrent();
+        if (currentSettings.shouldInsertSpacesAutomatically()
+                && currentSettings.mCurrentLanguageHasSpaces
                 && !mConnection.textBeforeCursorLooksLikeURL()) {
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.latinIME_promotePhantomSpace();
@@ -2887,6 +2928,12 @@
         return mSuggest.hasMainDictionary();
     }
 
+    // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
+    @UsedForTesting
+    /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
+        mSuggest.resetMainDict(this, locale, null);
+    }
+
     public void debugDumpStateAndCrashWithException(final String context) {
         final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
         s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index ed6fefa..b2bb615 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -75,7 +75,8 @@
 
     public UserBinaryDictionary(final Context context, final String locale,
             final boolean alsoUseMoreRestrictiveLocales) {
-        super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
+        super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER,
+                false /* isUpdatable */);
         if (null == locale) throw new NullPointerException(); // Catch the error earlier
         if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) {
             // If we don't have a locale, insert into the "all locales" user dictionary.
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java
index 046c5b5..5e3d6d2 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoder.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.makedict.decoder.HeaderReaderInterface;
 import com.android.inputmethod.latin.utils.JniUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -250,7 +251,7 @@
         /**
          * Reads a string from a buffer. This is the converse of the above method.
          */
-        private static String readString(final FusionDictionaryBufferInterface buffer) {
+        static String readString(final FusionDictionaryBufferInterface buffer) {
             final StringBuilder s = new StringBuilder();
             int character = readChar(buffer);
             while (character != FormatSpec.INVALID_CHARACTER) {
@@ -457,16 +458,13 @@
         return result;
     }
 
-    // TODO: static!? This will behave erratically when used in multi-threaded code.
-    // We need to fix this
-    private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
     @SuppressWarnings("unused")
     private static WeightedString getWordAtAddressWithParentAddress(
             final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
             final FormatOptions options) {
         int currentAddress = address;
-        int index = FormatSpec.MAX_WORD_LENGTH - 1;
         int frequency = Integer.MIN_VALUE;
+        final StringBuilder builder = new StringBuilder();
         // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH
         for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) {
             CharGroupInfo currentInfo;
@@ -482,17 +480,12 @@
                 }
             } while (BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags, options));
             if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency;
-            for (int i = 0; i < currentInfo.mCharacters.length; ++i) {
-                sGetWordBuffer[index--] =
-                        currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1];
-            }
+            builder.insert(0,
+                    new String(currentInfo.mCharacters, 0, currentInfo.mCharacters.length));
             if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break;
             currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
         }
-
-        return new WeightedString(
-                new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1),
-                        frequency);
+        return new WeightedString(builder.toString(), frequency);
     }
 
     private static WeightedString getWordAtAddressWithoutParentAddress(
@@ -637,7 +630,7 @@
      * @throws UnsupportedFormatException
      * @throws IOException
      */
-    private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer)
+    static int checkFormatVersion(final FusionDictionaryBufferInterface buffer)
             throws IOException, UnsupportedFormatException {
         final int version = getFormatVersion(buffer);
         if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
@@ -651,25 +644,22 @@
 
     /**
      * Reads a header from a buffer.
-     * @param buffer the buffer to read.
+     * @param headerReader the header reader
      * @throws IOException
      * @throws UnsupportedFormatException
      */
-    public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer)
+    public static FileHeader readHeader(final HeaderReaderInterface headerReader)
             throws IOException, UnsupportedFormatException {
-        final int version = checkFormatVersion(buffer);
-        final int optionsFlags = buffer.readUnsignedShort();
+        final int version = headerReader.readVersion();
+        final int optionsFlags = headerReader.readOptionFlags();
 
-        final HashMap<String, String> attributes = new HashMap<String, String>();
-        final int headerSize;
-        headerSize = buffer.readInt();
+        final int headerSize = headerReader.readHeaderSize();
 
         if (headerSize < 0) {
             throw new UnsupportedFormatException("header size can't be negative.");
         }
 
-        populateOptions(buffer, headerSize, attributes);
-        buffer.position(headerSize);
+        final HashMap<String, String> attributes = headerReader.readAttributes(headerSize);
 
         final FileHeader header = new FileHeader(headerSize,
                 new FusionDictionary.DictionaryOptions(attributes,
@@ -719,14 +709,14 @@
         }
 
         // Read header
-        final FileHeader header = readHeader(reader.getBuffer());
+        final FileHeader fileHeader = readHeader(reader);
 
         Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>();
         Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
-        final PtNodeArray root = readNodeArray(reader.getBuffer(), header.mHeaderSize,
-                reverseNodeArrayMapping, reverseGroupMapping, header.mFormatOptions);
+        final PtNodeArray root = readNodeArray(reader.getBuffer(), fileHeader.mHeaderSize,
+                reverseNodeArrayMapping, reverseGroupMapping, fileHeader.mFormatOptions);
 
-        FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions);
+        FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions);
         if (null != dict) {
             for (final Word w : dict) {
                 if (w.mIsBlacklistEntry) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index 476d51b..e5735aa 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -24,13 +24,13 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.ByteArrayWrapper;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.Map;
@@ -141,20 +141,20 @@
      * Reads unigrams and bigrams from the binary file.
      * Doesn't store a full memory representation of the dictionary.
      *
-     * @param reader the reader.
+     * @param dictReader the dict reader.
      * @param words the map to store the address as a key and the word as a value.
      * @param frequencies the map to store the address as a key and the frequency as a value.
      * @param bigrams the map to store the address as a key and the list of address as a value.
      * @throws IOException if the file can't be read.
      * @throws UnsupportedFormatException if the format of the file is not recognized.
      */
-    public static void readUnigramsAndBigramsBinary(final BinaryDictReader reader,
+    public static void readUnigramsAndBigramsBinary(final BinaryDictReader dictReader,
             final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
             final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException,
             UnsupportedFormatException {
         // Read header
-        final FileHeader header = BinaryDictDecoder.readHeader(reader.getBuffer());
-        readUnigramsAndBigramsBinaryInner(reader.getBuffer(), header.mHeaderSize, words,
+        final FileHeader header = BinaryDictDecoder.readHeader(dictReader);
+        readUnigramsAndBigramsBinaryInner(dictReader.getBuffer(), header.mHeaderSize, words,
                 frequencies, bigrams, header.mFormatOptions);
     }
 
@@ -162,19 +162,20 @@
      * Gets the address of the last CharGroup of the exact matching word in the dictionary.
      * If no match is found, returns NOT_VALID_WORD.
      *
-     * @param buffer the buffer to read.
+     * @param dictReader the dict reader.
      * @param word the word we search for.
      * @return the address of the terminal node.
      * @throws IOException if the file can't be read.
      * @throws UnsupportedFormatException if the format of the file is not recognized.
      */
     @UsedForTesting
-    public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer,
+    public static int getTerminalPosition(final BinaryDictReader dictReader,
             final String word) throws IOException, UnsupportedFormatException {
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         if (word == null) return FormatSpec.NOT_VALID_WORD;
         if (buffer.position() != 0) buffer.position(0);
 
-        final FileHeader header = BinaryDictDecoder.readHeader(buffer);
+        final FileHeader header = BinaryDictDecoder.readHeader(dictReader);
         int wordPos = 0;
         final int wordLen = word.codePointCount(0, word.length());
         for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
@@ -507,21 +508,22 @@
     }
 
     /**
-     * Find a word from the buffer.
+     * Find a word using the BinaryDictReader.
      *
-     * @param buffer the buffer representing the body of the dictionary file.
+     * @param dictReader the dict reader
      * @param word the word searched
      * @return the found group
      * @throws IOException
      * @throws UnsupportedFormatException
      */
     @UsedForTesting
-    public static CharGroupInfo findWordFromBuffer(final FusionDictionaryBufferInterface buffer,
+    public static CharGroupInfo findWordByBinaryDictReader(final BinaryDictReader dictReader,
             final String word) throws IOException, UnsupportedFormatException {
-        int position = getTerminalPosition(buffer, word);
+        int position = getTerminalPosition(dictReader, word);
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         if (position != FormatSpec.NOT_VALID_WORD) {
             buffer.position(0);
-            final FileHeader header = BinaryDictDecoder.readHeader(buffer);
+            final FileHeader header = BinaryDictDecoder.readHeader(dictReader);
             buffer.position(position);
             return BinaryDictDecoder.readCharGroup(buffer, position, header.mFormatOptions);
         }
@@ -542,16 +544,21 @@
             final File file, final long offset, final long length)
             throws FileNotFoundException, IOException, UnsupportedFormatException {
         final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE];
-        final FileInputStream inStream = new FileInputStream(file);
-        try {
-            inStream.read(buffer);
-            final BinaryDictDecoder.ByteBufferWrapper wrapper =
-                    new BinaryDictDecoder.ByteBufferWrapper(inStream.getChannel().map(
-                            FileChannel.MapMode.READ_ONLY, offset, length));
-            return BinaryDictDecoder.readHeader(wrapper);
-        } finally {
-            inStream.close();
-        }
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
+        dictReader.openBuffer(new BinaryDictReader.FusionDictionaryBufferFactory() {
+            @Override
+            public FusionDictionaryBufferInterface getFusionDictionaryBuffer(File file)
+                    throws FileNotFoundException, IOException {
+                final FileInputStream inStream = new FileInputStream(file);
+                try {
+                    inStream.read(buffer);
+                    return new ByteArrayWrapper(buffer);
+                } finally {
+                    inStream.close();
+                }
+            }
+        });
+        return BinaryDictDecoder.readHeader(dictReader);
     }
 
     public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset,
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
index f2f3c46..6d3b31a 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictReader.java
@@ -17,7 +17,9 @@
 package com.android.inputmethod.latin.makedict;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoder.CharEncoding;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoder.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.decoder.HeaderReaderInterface;
 import com.android.inputmethod.latin.utils.ByteArrayWrapper;
 
 import java.io.File;
@@ -27,8 +29,9 @@
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
+import java.util.HashMap;
 
-public class BinaryDictReader {
+public class BinaryDictReader implements HeaderReaderInterface {
 
     public interface FusionDictionaryBufferFactory {
         public FusionDictionaryBufferInterface getFusionDictionaryBuffer(final File file)
@@ -133,4 +136,34 @@
         openBuffer(factory);
         return getBuffer();
     }
+
+    // The implementation of HeaderReaderInterface
+    @Override
+    public int readVersion() throws IOException, UnsupportedFormatException {
+        return BinaryDictDecoder.checkFormatVersion(mFusionDictionaryBuffer);
+    }
+
+    @Override
+    public int readOptionFlags() {
+        return mFusionDictionaryBuffer.readUnsignedShort();
+    }
+
+    @Override
+    public int readHeaderSize() {
+        return mFusionDictionaryBuffer.readInt();
+    }
+
+    @Override
+    public HashMap<String, String> readAttributes(final int headerSize) {
+        final HashMap<String, String> attributes = new HashMap<String, String>();
+        while (mFusionDictionaryBuffer.position() < headerSize) {
+            // We can avoid infinite loop here since mFusionDictonary.position() is always increased
+            // by calling CharEncoding.readString.
+            final String key = CharEncoding.readString(mFusionDictionaryBuffer);
+            final String value = CharEncoding.readString(mFusionDictionaryBuffer);
+            attributes.put(key, value);
+        }
+        mFusionDictionaryBuffer.position(headerSize);
+        return attributes;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
index 5d116d7..584b793 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -49,17 +49,18 @@
     /**
      * Delete the word from the binary file.
      *
-     * @param buffer the buffer to write.
+     * @param dictReader the dict reader.
      * @param word the word we delete
      * @throws IOException
      * @throws UnsupportedFormatException
      */
     @UsedForTesting
-    public static void deleteWord(final FusionDictionaryBufferInterface buffer,
-            final String word) throws IOException, UnsupportedFormatException {
+    public static void deleteWord(final BinaryDictReader dictReader, final String word)
+            throws IOException, UnsupportedFormatException {
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         buffer.position(0);
-        final FileHeader header = BinaryDictDecoder.readHeader(buffer);
-        final int wordPosition = BinaryDictIOUtils.getTerminalPosition(buffer, word);
+        final FileHeader header = BinaryDictDecoder.readHeader(dictReader);
+        final int wordPosition = BinaryDictIOUtils.getTerminalPosition(dictReader, word);
         if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
 
         buffer.position(wordPosition);
@@ -235,7 +236,7 @@
     /**
      * Insert a word into a binary dictionary.
      *
-     * @param buffer the buffer containing the existing dictionary.
+     * @param dictReader the dict reader.
      * @param destination a stream to the underlying file, with the pointer at the end of the file.
      * @param word the word to insert.
      * @param frequency the frequency of the new word.
@@ -248,16 +249,16 @@
     // TODO: Support batch insertion.
     // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
     @UsedForTesting
-    public static void insertWord(final FusionDictionaryBufferInterface buffer,
-            final OutputStream destination, final String word, final int frequency,
-            final ArrayList<WeightedString> bigramStrings,
+    public static void insertWord(final BinaryDictReader dictReader, final OutputStream destination,
+            final String word, final int frequency, final ArrayList<WeightedString> bigramStrings,
             final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
             final boolean isBlackListEntry)
                     throws IOException, UnsupportedFormatException {
         final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         if (bigramStrings != null) {
             for (final WeightedString bigram : bigramStrings) {
-                int position = BinaryDictIOUtils.getTerminalPosition(buffer, bigram.mWord);
+                int position = BinaryDictIOUtils.getTerminalPosition(dictReader, bigram.mWord);
                 if (position == FormatSpec.NOT_VALID_WORD) {
                     // TODO: figure out what is the correct thing to do here.
                 } else {
@@ -272,7 +273,7 @@
 
         // find the insert position of the word.
         if (buffer.position() != 0) buffer.position(0);
-        final FileHeader header = BinaryDictDecoder.readHeader(buffer);
+        final FileHeader fileHeader = BinaryDictDecoder.readHeader(dictReader);
 
         int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.position();
         final int[] codePoints = FusionDictionary.getCodePoints(word);
@@ -288,9 +289,9 @@
             for (int i = 0; i < charGroupCount; ++i) {
                 address = buffer.position();
                 final CharGroupInfo currentInfo = BinaryDictDecoder.readCharGroup(buffer,
-                        buffer.position(), header.mFormatOptions);
+                        buffer.position(), fileHeader.mFormatOptions);
                 final boolean isMovedGroup = BinaryDictIOUtils.isMovedGroup(currentInfo.mFlags,
-                        header.mFormatOptions);
+                        fileHeader.mFormatOptions);
                 if (isMovedGroup) continue;
                 nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS)
                         ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address;
@@ -310,16 +311,16 @@
                         final int newNodeAddress = buffer.limit();
                         final int flags = BinaryDictEncoder.makeCharGroupFlags(p > 1,
                                 isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
-                                false /* isBlackListEntry */, header.mFormatOptions);
+                                false /* isBlackListEntry */, fileHeader.mFormatOptions);
                         int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags,
                                 frequency, nodeParentAddress, shortcuts, bigrams, destination,
-                                buffer, nodeOriginAddress, address, header.mFormatOptions);
+                                buffer, nodeOriginAddress, address, fileHeader.mFormatOptions);
 
                         final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
                                 currentInfo.mCharacters.length);
                         if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
                             updateParentAddresses(buffer, currentInfo.mChildrenAddress,
-                                    newNodeAddress + written + 1, header.mFormatOptions);
+                                    newNodeAddress + written + 1, fileHeader.mFormatOptions);
                         }
                         final CharGroupInfo newInfo2 = new CharGroupInfo(
                                 newNodeAddress + written + 1, -1 /* endAddress */,
@@ -351,17 +352,17 @@
                                     false /* isTerminal */, 0 /* childrenAddressSize*/,
                                     false /* hasShortcut */, false /* hasBigrams */,
                                     false /* isNotAWord */, false /* isBlackListEntry */,
-                                    header.mFormatOptions);
+                                    fileHeader.mFormatOptions);
                             int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p,
                                     prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
                                     destination, buffer, nodeOriginAddress, address,
-                                    header.mFormatOptions);
+                                    fileHeader.mFormatOptions);
 
                             final int[] suffixCharacters = Arrays.copyOfRange(
                                     currentInfo.mCharacters, p, currentInfo.mCharacters.length);
                             if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
                                 updateParentAddresses(buffer, currentInfo.mChildrenAddress,
-                                        newNodeAddress + written + 1, header.mFormatOptions);
+                                        newNodeAddress + written + 1, fileHeader.mFormatOptions);
                             }
                             final int suffixFlags = BinaryDictEncoder.makeCharGroupFlags(
                                     suffixCharacters.length > 1,
@@ -370,21 +371,21 @@
                                     (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)
                                             != 0,
                                     (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0,
-                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
+                                    isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
                             final CharGroupInfo suffixInfo = new CharGroupInfo(
                                     newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
                                     suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
                                     currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
                                     currentInfo.mBigrams);
                             written += BinaryDictIOUtils.computeGroupSize(suffixInfo,
-                                    header.mFormatOptions) + 1;
+                                    fileHeader.mFormatOptions) + 1;
 
                             final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
                                     codePoints.length);
                             final int flags = BinaryDictEncoder.makeCharGroupFlags(
                                     newCharacters.length > 1, isTerminal,
                                     0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                    isNotAWord, isBlackListEntry, header.mFormatOptions);
+                                    isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
                             final CharGroupInfo newInfo = new CharGroupInfo(
                                     newNodeAddress + written, -1 /* endAddress */, flags,
                                     newCharacters, frequency, newNodeAddress + 1,
@@ -406,13 +407,13 @@
                         final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
                         final int flags = BinaryDictEncoder.makeCharGroupFlags(hasMultipleChars,
                                 isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                isNotAWord, isBlackListEntry, header.mFormatOptions);
+                                isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
                         final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
                                 -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
                                 nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
                                 bigrams);
                         moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address,
-                                header.mFormatOptions);
+                                fileHeader.mFormatOptions);
                         return;
                     }
                     wordPos += currentInfo.mCharacters.length;
@@ -431,12 +432,12 @@
                          */
                         final int newNodeAddress = buffer.limit();
                         updateChildrenAddress(buffer, address, newNodeAddress,
-                                header.mFormatOptions);
+                                fileHeader.mFormatOptions);
                         final int newGroupAddress = newNodeAddress + 1;
                         final boolean hasMultipleChars = (wordLen - wordPos) > 1;
                         final int flags = BinaryDictEncoder.makeCharGroupFlags(hasMultipleChars,
                                 isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                                isNotAWord, isBlackListEntry, header.mFormatOptions);
+                                isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
                         final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
                         final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags,
                                 characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
@@ -481,7 +482,7 @@
                 final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
                 final int flags = BinaryDictEncoder.makeCharGroupFlags(characters.length > 1,
                         isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
-                        isNotAWord, isBlackListEntry, header.mFormatOptions);
+                        isNotAWord, isBlackListEntry, fileHeader.mFormatOptions);
                 final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
                         -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
                         FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
diff --git a/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java b/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java
new file mode 100644
index 0000000..7cddef2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/decoder/HeaderReaderInterface.java
@@ -0,0 +1,32 @@
+/*
+ * 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.makedict.decoder;
+
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * An interface to read a binary dictionary file header.
+ */
+public interface HeaderReaderInterface {
+    public int readVersion() throws IOException, UnsupportedFormatException;
+    public int readOptionFlags();
+    public int readHeaderSize();
+    public HashMap<String, String> readAttributes(final int headerSize);
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
index 065e00e..525d3cd 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPredictionDictionaryBase.java
@@ -43,6 +43,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
@@ -75,6 +76,8 @@
     private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions =
             CollectionUtils.newArrayList();
 
+    private final AtomicReference<AsyncTask<Void, Void, Void>> mWaitingTask;
+
     // Should always be false except when we use this class for test
     @UsedForTesting boolean mIsTest = false;
 
@@ -83,6 +86,7 @@
         super(context, dictionaryType);
         mLocale = locale;
         mPrefs = sp;
+        mWaitingTask = new AtomicReference<AsyncTask<Void, Void, Void>>();
         if (mLocale != null && mLocale.length() > 1) {
             loadDictionary();
         }
@@ -174,7 +178,11 @@
      */
     private void flushPendingWrites() {
         // Create a background thread to write the pending entries
-        new UpdateBinaryTask(mBigramList, mLocale, this, mPrefs, getContext()).execute();
+        final AsyncTask<Void, Void, Void> old = mWaitingTask.getAndSet(new UpdateBinaryTask(
+                mBigramList, mLocale, this, mPrefs, getContext()).execute());
+        if (old != null) {
+            old.cancel(false);
+        }
     }
 
     @Override
@@ -287,6 +295,7 @@
 
         @Override
         protected Void doInBackground(final Void... v) {
+            if (isCancelled()) return null;
             if (mDynamicPredictionDictionary.mIsTest) {
                 // If mIsTest == true, wait until the lock is released.
                 mDynamicPredictionDictionary.mBigramListLock.lock();
@@ -306,6 +315,9 @@
         }
 
         private void doWriteTaskLocked() {
+            if (isCancelled()) return;
+            mDynamicPredictionDictionary.mWaitingTask.compareAndSet(this, null);
+
             if (DBG_STRESS_TEST) {
                 try {
                     Log.w(TAG, "Start stress in closing: " + mLocale);
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
index e38a235..275ce2f 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -36,7 +36,9 @@
 
     // Singleton
     private PersonalizationDictionary(final Context context, final String locale) {
-        super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION);
+        // TODO: Make isUpdatable true.
+        super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION,
+                false /* isUpdatable */);
         mLocale = locale;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
index 534d3c5..da59333 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
@@ -25,4 +25,10 @@
 
     public static void onConfigurationChanged(final Context context, final Configuration conf) {
     }
+
+    public static void onUpdateData(Context context, String type) {
+    }
+
+    public static void onRemoveData(Context context, String type) {
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
index 858aa32..433c69c 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.personalization;
 
+import android.content.Context;
+
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
@@ -48,7 +50,7 @@
 
     public abstract void onDictionaryReady();
 
-    public abstract void onDictionaryClosed();
+    public abstract void onDictionaryClosed(Context context);
 
     public void setPredictionDictionary(String locale, DynamicPredictionDictionaryBase dictionary) {
         mPredictionDictionary = new WeakReference<DynamicPredictionDictionaryBase>(dictionary);
@@ -68,9 +70,9 @@
     }
 
 
-    public void closeSession() {
+    public void closeSession(Context context) {
         unsetPredictionDictionary();
-        onDictionaryClosed();
+        onDictionaryClosed(context);
     }
 
     public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid,
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
index 955bd27..a038d0a 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.personalization;
 
 import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -31,6 +32,6 @@
 
     @Override
     protected String getDictionaryFileName() {
-        return NAME + "." + getLocale() + ".dict";
+        return NAME + "." + getLocale() + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
index d117844..76e48c7 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.personalization;
 
 import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -26,7 +27,8 @@
  * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
  */
 public class UserHistoryPredictionDictionary extends DynamicPredictionDictionaryBase {
-    private static final String NAME = UserHistoryPredictionDictionary.class.getSimpleName();
+    /* package for tests */ static final String NAME =
+            UserHistoryPredictionDictionary.class.getSimpleName();
     /* package */ UserHistoryPredictionDictionary(final Context context, final String locale,
             final SharedPreferences sp) {
         super(context, locale, sp, Dictionary.TYPE_USER_HISTORY);
@@ -34,6 +36,6 @@
 
     @Override
     protected String getDictionaryFileName() {
-        return NAME + "." + getLocale() + ".dict";
+        return NAME + "." + getLocale() + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 195f9f8..a0b744d 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -57,6 +57,7 @@
     public final SuggestedWords mSuggestPuncList;
     public final String mWordSeparators;
     public final CharSequence mHintToSaveText;
+    public final boolean mCurrentLanguageHasSpaces;
 
     // From preferences, in the same order as xml/prefs.xml:
     public final boolean mAutoCap;
@@ -118,6 +119,7 @@
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mWordSeparators = res.getString(R.string.symbols_word_separators);
         mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
+        mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
 
         // Store the input attributes
         if (null == inputAttributes) {
@@ -186,6 +188,7 @@
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\"";
         mHintToSaveText = "Touch again to save";
+        mCurrentLanguageHasSpaces = true;
         mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
         mAutoCap = true;
         mVibrateOn = true;
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index 98f0d8b..cc25102 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -18,6 +18,7 @@
 
 import android.util.SparseArray;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -94,6 +95,10 @@
         return new CopyOnWriteArrayList<E>(array);
     }
 
+    public static <E> ArrayDeque<E> newArrayDeque() {
+        return new ArrayDeque<E>();
+    }
+
     public static <E> SparseArray<E> newSparseArray() {
         return new SparseArray<E>();
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 20cc5d8..be41840 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -370,12 +370,19 @@
         return sb.toString();
     }
 
+    /**
+     * Convert hex string to byte array. The string length must be an even number.
+     */
     @UsedForTesting
     public static byte[] hexStringToByteArray(String hexString) {
         if (TextUtils.isEmpty(hexString)) {
             return null;
         }
         final int N = hexString.length();
+        if (N % 2 != 0) {
+            throw new NumberFormatException("Input hex string length must be an even number."
+                    + " Length = " + N);
+        }
         final byte[] bytes = new byte[N / 2];
         for (int i = 0; i < N; i += 2) {
             bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index d27a7a9..6cc4bef 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.test.suitebuilder.annotation.LargeTest;
+import android.view.inputmethod.BaseInputConnection;
 
 @LargeTest
 public class InputLogicTests extends InputTestsBase {
@@ -290,5 +291,19 @@
         }
         assertEquals("delete whole composing word", "", mEditText.getText().toString());
     }
+
+    public void testResumeSuggestionOnBackspace() {
+        final String WORD_TO_TYPE = "and this ";
+        type(WORD_TO_TYPE);
+        assertEquals("resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+        type(Constants.CODE_DELETE);
+        assertEquals("resume suggestion on backspace", 4,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("resume suggestion on backspace", 8,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+    }
     // TODO: Add some tests for non-BMP characters
 }
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java
new file mode 100644
index 0000000..0f0ebaf
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java
@@ -0,0 +1,105 @@
+/*
+ * 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.test.suitebuilder.annotation.LargeTest;
+import android.view.inputmethod.BaseInputConnection;
+
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+
+@LargeTest
+public class InputLogicTestsLanguageWithoutSpaces extends InputTestsBase {
+    public void testAutoCorrectForLanguageWithoutSpaces() {
+        final String STRING_TO_TYPE = "tgis is";
+        final String EXPECTED_RESULT = "thisis";
+        changeKeyboardLocaleAndDictLocale("th", "en_US");
+        type(STRING_TO_TYPE);
+        assertEquals("simple auto-correct for language without spaces", EXPECTED_RESULT,
+                mEditText.getText().toString());
+    }
+
+    public void testRevertAutoCorrectForLanguageWithoutSpaces() {
+        final String STRING_TO_TYPE = "tgis ";
+        final String EXPECTED_INTERMEDIATE_RESULT = "this";
+        final String EXPECTED_FINAL_RESULT = "tgis";
+        changeKeyboardLocaleAndDictLocale("th", "en_US");
+        type(STRING_TO_TYPE);
+        assertEquals("simple auto-correct for language without spaces",
+                EXPECTED_INTERMEDIATE_RESULT, mEditText.getText().toString());
+        type(Constants.CODE_DELETE);
+        assertEquals("simple auto-correct for language without spaces",
+                EXPECTED_FINAL_RESULT, mEditText.getText().toString());
+        // Check we are back to composing the word
+        assertEquals("don't resume suggestion on backspace", 0,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("don't resume suggestion on backspace", 4,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+    }
+
+    public void testDontResumeSuggestionOnBackspace() {
+        final String WORD_TO_TYPE = "and this ";
+        changeKeyboardLocaleAndDictLocale("th", "en_US");
+        type(WORD_TO_TYPE);
+        assertEquals("don't resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("don't resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+        type(" ");
+        type(Constants.CODE_DELETE);
+        assertEquals("don't resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("don't resume suggestion on backspace", -1,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+    }
+
+    public void testStartComposingInsideText() {
+        final String WORD_TO_TYPE = "abcdefgh ";
+        final int typedLength = WORD_TO_TYPE.length() - 1; // -1 because space gets eaten
+        final int CURSOR_POS = 4;
+        changeKeyboardLocaleAndDictLocale("th", "en_US");
+        type(WORD_TO_TYPE);
+        mLatinIME.onUpdateSelection(0, 0, typedLength, typedLength, -1, -1);
+        mInputConnection.setSelection(CURSOR_POS, CURSOR_POS);
+        mLatinIME.onUpdateSelection(typedLength, typedLength,
+                CURSOR_POS, CURSOR_POS, -1, -1);
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        assertEquals("start composing inside text", -1,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("start composing inside text", -1,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+        type("xxxx");
+        assertEquals("start composing inside text", 4,
+                BaseInputConnection.getComposingSpanStart(mEditText.getText()));
+        assertEquals("start composing inside text", 8,
+                BaseInputConnection.getComposingSpanEnd(mEditText.getText()));
+    }
+
+    public void testPredictions() {
+        final String WORD_TO_TYPE = "Barack ";
+        changeKeyboardLocaleAndDictLocale("th", "en_US");
+        type(WORD_TO_TYPE);
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        // Make sure there is no space
+        assertEquals("predictions in lang without spaces", "Barack",
+                mEditText.getText().toString());
+        // Test the first prediction is displayed
+        assertEquals("predictions in lang without spaces", "Obama",
+                mLatinIME.getFirstSuggestedWord());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index cc3e0d7..480570e 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -46,6 +46,8 @@
 
     // The message that sets the underline is posted with a 100 ms delay
     protected static final int DELAY_TO_WAIT_FOR_UNDERLINE = 200;
+    // The message that sets predictions is posted with a 100 ms delay
+    protected static final int DELAY_TO_WAIT_FOR_PREDICTIONS = 200;
 
     protected LatinIME mLatinIME;
     protected Keyboard mKeyboard;
@@ -233,9 +235,6 @@
                 --remainingAttempts;
             }
         }
-        if (!mLatinIME.hasMainDictionary()) {
-            throw new RuntimeException("Can't initialize the main dictionary");
-        }
     }
 
     protected void changeLanguage(final String locale) {
@@ -247,6 +246,16 @@
         waitForDictionaryToBeLoaded();
     }
 
+    protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale,
+            final String dictLocale) {
+        changeLanguage(keyboardLocale);
+        if (!keyboardLocale.equals(dictLocale)) {
+            mLatinIME.replaceMainDictionaryForTest(
+                    LocaleUtils.constructLocaleFromString(dictLocale));
+        }
+        waitForDictionaryToBeLoaded();
+    }
+
     protected void pickSuggestionManually(final int index, final String suggestion) {
         mLatinIME.pickSuggestionManually(index, new SuggestedWordInfo(suggestion, 1,
                 SuggestedWordInfo.KIND_CORRECTION, "main"));
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 6d37466..be468c1 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -71,6 +71,8 @@
     private static final FormatSpec.FormatOptions VERSION3_WITH_DYNAMIC_UPDATE =
             new FormatSpec.FormatOptions(3, true /* supportsDynamicUpdate */);
 
+    private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
+
     public BinaryDictDecoderEncoderTests() {
         this(System.currentTimeMillis(), DEFAULT_MAX_UNIGRAMS);
     }
@@ -293,7 +295,8 @@
             final String message) {
         File file = null;
         try {
-            file = File.createTempFile("runReadAndWrite", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("runReadAndWrite", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
             Log.e(TAG, "IOException", e);
         }
@@ -435,7 +438,8 @@
             final FormatSpec.FormatOptions formatOptions, final String message) {
         File file = null;
         try {
-            file = File.createTempFile("runReadUnigrams", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("runReadUnigrams", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
             Log.e(TAG, "IOException", e);
         }
@@ -493,31 +497,31 @@
     }
 
     // Tests for getTerminalPosition
-    private String getWordFromBinary(final FusionDictionaryBufferInterface buffer,
-            final int address) {
+    private String getWordFromBinary(final BinaryDictReader dictReader, final int address) {
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         if (buffer.position() != 0) buffer.position(0);
 
-        FileHeader header = null;
+        FileHeader fileHeader = null;
         try {
-            header = BinaryDictDecoder.readHeader(buffer);
+            fileHeader = BinaryDictDecoder.readHeader(dictReader);
         } catch (IOException e) {
             return null;
         } catch (UnsupportedFormatException e) {
             return null;
         }
-        if (header == null) return null;
-        return BinaryDictDecoder.getWordAtAddress(buffer, header.mHeaderSize,
-                address - header.mHeaderSize, header.mFormatOptions).mWord;
+        if (fileHeader == null) return null;
+        return BinaryDictDecoder.getWordAtAddress(buffer, fileHeader.mHeaderSize,
+                address - fileHeader.mHeaderSize, fileHeader.mFormatOptions).mWord;
     }
 
-    private long runGetTerminalPosition(final FusionDictionaryBufferInterface buffer,
-            final String word, int index, boolean contained) {
+    private long runGetTerminalPosition(final BinaryDictReader reader, final String word, int index,
+            boolean contained) {
         final int expectedFrequency = (UNIGRAM_FREQ + index) % 255;
         long diff = -1;
         int position = -1;
         try {
             final long now = System.nanoTime();
-            position = BinaryDictIOUtils.getTerminalPosition(buffer, word);
+            position = BinaryDictIOUtils.getTerminalPosition(reader, word);
             diff = System.nanoTime() - now;
         } catch (IOException e) {
             Log.e(TAG, "IOException while getTerminalPosition", e);
@@ -526,14 +530,14 @@
         }
 
         assertEquals(FormatSpec.NOT_VALID_WORD != position, contained);
-        if (contained) assertEquals(getWordFromBinary(buffer, position), word);
+        if (contained) assertEquals(getWordFromBinary(reader, position), word);
         return diff;
     }
 
     public void testGetTerminalPosition() {
         File file = null;
         try {
-            file = File.createTempFile("testGetTerminalPosition", ".dict",
+            file = File.createTempFile("testGetTerminalPosition", TEST_DICT_FILE_EXTENSION,
                     getContext().getCacheDir());
         } catch (IOException e) {
             // do nothing
@@ -547,29 +551,27 @@
         timeWritingDictToFile(file, dict, VERSION3_WITH_DYNAMIC_UPDATE);
 
         final BinaryDictReader reader = new BinaryDictReader(file);
-        FusionDictionaryBufferInterface buffer = null;
         try {
-            buffer = reader.openAndGetBuffer(
-                    new BinaryDictReader.FusionDictionaryBufferFromByteArrayFactory());
+            reader.openBuffer(new BinaryDictReader.FusionDictionaryBufferFromByteArrayFactory());
         } catch (IOException e) {
             // ignore
             Log.e(TAG, "IOException while opening the buffer", e);
         }
-        assertNotNull("Can't get the buffer", buffer);
+        assertNotNull("Can't get the buffer", reader.getBuffer());
 
         try {
             // too long word
             final String longWord = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, longWord));
+                    BinaryDictIOUtils.getTerminalPosition(reader, longWord));
 
             // null
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, null));
+                    BinaryDictIOUtils.getTerminalPosition(reader, null));
 
             // empty string
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, ""));
+                    BinaryDictIOUtils.getTerminalPosition(reader, ""));
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         }
@@ -577,7 +579,7 @@
         // Test a word that is contained within the dictionary.
         long sum = 0;
         for (int i = 0; i < sWords.size(); ++i) {
-            final long time = runGetTerminalPosition(buffer, sWords.get(i), i, true);
+            final long time = runGetTerminalPosition(reader, sWords.get(i), i, true);
             sum += time == -1 ? 0 : time;
         }
         Log.d(TAG, "per a search : " + (((double)sum) / sWords.size() / 1000000));
@@ -588,14 +590,15 @@
         for (int i = 0; i < 1000; ++i) {
             final String word = generateWord(random, codePointSet);
             if (sWords.indexOf(word) != -1) continue;
-            runGetTerminalPosition(buffer, word, i, false);
+            runGetTerminalPosition(reader, word, i, false);
         }
     }
 
     public void testDeleteWord() {
         File file = null;
         try {
-            file = File.createTempFile("testDeleteWord", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("testDeleteWord", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
             // do nothing
         }
@@ -608,28 +611,27 @@
         timeWritingDictToFile(file, dict, VERSION3_WITH_DYNAMIC_UPDATE);
 
         final BinaryDictReader reader = new BinaryDictReader(file);
-        FusionDictionaryBufferInterface buffer = null;
         try {
-            buffer = reader.openAndGetBuffer(
+            reader.openBuffer(
                     new BinaryDictReader.FusionDictionaryBufferFromByteArrayFactory());
         } catch (IOException e) {
             // ignore
             Log.e(TAG, "IOException while opening the buffer", e);
         }
-        assertNotNull("Can't get the buffer", buffer);
+        assertNotNull("Can't get the buffer", reader.getBuffer());
 
         try {
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
-            DynamicBinaryDictIOUtils.deleteWord(buffer, sWords.get(0));
+                    BinaryDictIOUtils.getTerminalPosition(reader, sWords.get(0)));
+            DynamicBinaryDictIOUtils.deleteWord(reader, sWords.get(0));
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(0)));
+                    BinaryDictIOUtils.getTerminalPosition(reader, sWords.get(0)));
 
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
-            DynamicBinaryDictIOUtils.deleteWord(buffer, sWords.get(5));
+                    BinaryDictIOUtils.getTerminalPosition(reader, sWords.get(5)));
+            DynamicBinaryDictIOUtils.deleteWord(reader, sWords.get(5));
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    BinaryDictIOUtils.getTerminalPosition(buffer, sWords.get(5)));
+                    BinaryDictIOUtils.getTerminalPosition(reader, sWords.get(5)));
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         }
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index 011d711..bcf2c31 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -21,8 +21,9 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
-import com.android.inputmethod.latin.makedict.BinaryDictDecoder.ByteBufferWrapper;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoder.FusionDictionaryBufferInterface;
+import com.android.inputmethod.latin.makedict.BinaryDictReader.
+        FusionDictionaryBufferFromWritableByteBufferFactory;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
@@ -33,8 +34,6 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Random;
@@ -49,6 +48,8 @@
     public static final int DEFAULT_MAX_UNIGRAMS = 1500;
     private final int mMaxUnigrams;
 
+    private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
+
     private static final String[] CHARACTERS = {
         "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
         "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
@@ -127,22 +128,24 @@
         }
     }
 
-    private static void printBinaryFile(final FusionDictionaryBufferInterface buffer)
+    private static void printBinaryFile(final BinaryDictReader dictReader)
             throws IOException, UnsupportedFormatException {
-        FileHeader header = BinaryDictDecoder.readHeader(buffer);
+        final FileHeader fileHeader = BinaryDictDecoder.readHeader(dictReader);
+        final FusionDictionaryBufferInterface buffer = dictReader.getBuffer();
         while (buffer.position() < buffer.limit()) {
-            printNode(buffer, header.mFormatOptions);
+            printNode(buffer, fileHeader.mFormatOptions);
         }
     }
 
     private int getWordPosition(final File file, final String word) {
         int position = FormatSpec.NOT_VALID_WORD;
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
         FileInputStream inStream = null;
         try {
             inStream = new FileInputStream(file);
-            final FusionDictionaryBufferInterface buffer = new ByteBufferWrapper(
-                    inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()));
-            position = BinaryDictIOUtils.getTerminalPosition(buffer, word);
+            dictReader.openBuffer(
+                    new BinaryDictReader.FusionDictionaryBufferFromByteBufferFactory());
+            position = BinaryDictIOUtils.getTerminalPosition(dictReader, word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         } finally {
@@ -158,23 +161,14 @@
     }
 
     private CharGroupInfo findWordFromFile(final File file, final String word) {
-        FileInputStream inStream = null;
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
         CharGroupInfo info = null;
         try {
-            inStream = new FileInputStream(file);
-            final FusionDictionaryBufferInterface buffer = new ByteBufferWrapper(
-                    inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()));
-            info = BinaryDictIOUtils.findWordFromBuffer(buffer, word);
+            dictReader.openBuffer(
+                    new BinaryDictReader.FusionDictionaryBufferFromByteBufferFactory());
+            info = BinaryDictIOUtils.findWordByBinaryDictReader(dictReader, word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
-        } finally {
-            if (inStream != null) {
-                try {
-                    inStream.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
         }
         return info;
     }
@@ -183,42 +177,33 @@
     private long insertAndCheckWord(final File file, final String word, final int frequency,
             final boolean exist, final ArrayList<WeightedString> bigrams,
             final ArrayList<WeightedString> shortcuts) {
-        RandomAccessFile raFile = null;
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
         BufferedOutputStream outStream = null;
-        FusionDictionaryBufferInterface buffer = null;
         long amountOfTime = -1;
         try {
-            raFile = new RandomAccessFile(file, "rw");
-            buffer = new ByteBufferWrapper(raFile.getChannel().map(
-                    FileChannel.MapMode.READ_WRITE, 0, file.length()));
+            dictReader.openBuffer(new FusionDictionaryBufferFromWritableByteBufferFactory());
             outStream = new BufferedOutputStream(new FileOutputStream(file, true));
 
             if (!exist) {
                 assertEquals(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
             }
             final long now = System.nanoTime();
-            DynamicBinaryDictIOUtils.insertWord(buffer, outStream, word, frequency, bigrams,
+            DynamicBinaryDictIOUtils.insertWord(dictReader, outStream, word, frequency, bigrams,
                     shortcuts, false, false);
             amountOfTime = System.nanoTime() - now;
             outStream.flush();
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
             outStream.close();
-            raFile.close();
         } catch (IOException e) {
+            Log.e(TAG, "Raised an IOException while inserting a word", e);
         } catch (UnsupportedFormatException e) {
+            Log.e(TAG, "Raised an UnsupportedFormatException error while inserting a word", e);
         } finally {
             if (outStream != null) {
                 try {
                     outStream.close();
                 } catch (IOException e) {
-                    // do nothing
-                }
-            }
-            if (raFile != null) {
-                try {
-                    raFile.close();
-                } catch (IOException e) {
-                    // do nothing
+                    Log.e(TAG, "Failed to close the output stream", e);
                 }
             }
         }
@@ -226,52 +211,37 @@
     }
 
     private void deleteWord(final File file, final String word) {
-        RandomAccessFile raFile = null;
-        FusionDictionaryBufferInterface buffer = null;
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
         try {
-            raFile = new RandomAccessFile(file, "rw");
-            buffer = new ByteBufferWrapper(raFile.getChannel().map(
-                    FileChannel.MapMode.READ_WRITE, 0, file.length()));
-            DynamicBinaryDictIOUtils.deleteWord(buffer, word);
+            dictReader.openBuffer(new FusionDictionaryBufferFromWritableByteBufferFactory());
+            DynamicBinaryDictIOUtils.deleteWord(dictReader, word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
-        } finally {
-            if (raFile != null) {
-                try {
-                    raFile.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
         }
     }
 
     private void checkReverseLookup(final File file, final String word, final int position) {
-        FileInputStream inStream = null;
+        final BinaryDictReader dictReader = new BinaryDictReader(file);
         try {
-            inStream = new FileInputStream(file);
-            final FusionDictionaryBufferInterface buffer = new ByteBufferWrapper(
-                    inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, file.length()));
-            final FileHeader header = BinaryDictDecoder.readHeader(buffer);
-            assertEquals(word, BinaryDictDecoder.getWordAtAddress(buffer, header.mHeaderSize,
-                    position - header.mHeaderSize, header.mFormatOptions).mWord);
+            final FusionDictionaryBufferInterface buffer = dictReader.openAndGetBuffer(
+                    new BinaryDictReader.FusionDictionaryBufferFromByteBufferFactory());
+            final FileHeader fileHeader = BinaryDictDecoder.readHeader(dictReader);
+            assertEquals(word,
+                    BinaryDictDecoder.getWordAtAddress(dictReader.getBuffer(),
+                            fileHeader.mHeaderSize, position - fileHeader.mHeaderSize,
+                            fileHeader.mFormatOptions).mWord);
         } catch (IOException e) {
+            Log.e(TAG, "Raised an IOException while looking up a word", e);
         } catch (UnsupportedFormatException e) {
-        } finally {
-            if (inStream != null) {
-                try {
-                    inStream.close();
-                } catch (IOException e) {
-                    // do nothing
-                }
-            }
+            Log.e(TAG, "Raised an UnsupportedFormatException error while looking up a word", e);
         }
     }
 
     public void testInsertWord() {
         File file = null;
         try {
-            file = File.createTempFile("testInsertWord", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("testInsertWord", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
             fail("IOException while creating temporary file: " + e);
         }
@@ -321,7 +291,7 @@
     public void testInsertWordWithBigrams() {
         File file = null;
         try {
-            file = File.createTempFile("testInsertWordWithBigrams", ".dict",
+            file = File.createTempFile("testInsertWordWithBigrams", TEST_DICT_FILE_EXTENSION,
                     getContext().getCacheDir());
         } catch (IOException e) {
             fail("IOException while creating temporary file: " + e);
@@ -359,7 +329,8 @@
     public void testRandomWords() {
         File file = null;
         try {
-            file = File.createTempFile("testRandomWord", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("testRandomWord", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
         }
         assertNotNull(file);
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index b3e2ee0..99ccb1a 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -22,6 +22,7 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
+import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.io.File;
@@ -29,6 +30,7 @@
 import java.util.List;
 import java.util.Random;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Unit tests for UserHistoryDictionary
@@ -43,6 +45,8 @@
         "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
     };
 
+    private static final int MIN_USER_HISTORY_DICTIONARY_FILE_SIZE = 1000;
+
     @Override
     public void setUp() {
         mPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
@@ -78,43 +82,43 @@
         }
     }
 
+    private void addAndWriteRandomWords(final String testFilenameSuffix, final int numberOfWords,
+            final Random random) {
+        final List<String> words = generateWords(numberOfWords, random);
+        final UserHistoryPredictionDictionary dict =
+                PersonalizationDictionaryHelper.getUserHistoryPredictionDictionary(getContext(),
+                        testFilenameSuffix /* locale */, mPrefs);
+        // Add random words to the user history dictionary.
+        addToDict(dict, words);
+        // write to file.
+        dict.close();
+    }
+
     public void testRandomWords() {
         File dictFile = null;
+        Log.d(TAG, "This test can be used for profiling.");
+        Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
+        final String testFilenameSuffix = "testRandomWords" + System.currentTimeMillis();
+        final int numberOfWords = 1000;
+        final Random random = new Random(123456);
+
         try {
-            Log.d(TAG, "This test can be used for profiling.");
-            Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
-            final int numberOfWords = 1000;
-            final Random random = new Random(123456);
-            List<String> words = generateWords(numberOfWords, random);
-
-            final String locale = "testRandomWords";
-            final String fileName = "UserHistoryDictionary." + locale + ".dict";
-            dictFile = new File(getContext().getFilesDir(), fileName);
-            final UserHistoryPredictionDictionary dict =
-                    PersonalizationDictionaryHelper.getUserHistoryPredictionDictionary(
-                            getContext(), locale, mPrefs);
-            dict.mIsTest = true;
-
-            addToDict(dict, words);
-
-            try {
-                Log.d(TAG, "waiting for adding the word ...");
-                Thread.sleep(2000);
-            } catch (InterruptedException e) {
-                Log.d(TAG, "InterruptedException: " + e);
-            }
-
-            // write to file
-            dict.close();
-
+            addAndWriteRandomWords(testFilenameSuffix, numberOfWords, random);
+        } finally {
             try {
                 Log.d(TAG, "waiting for writing ...");
-                Thread.sleep(5000);
+                Thread.sleep(TimeUnit.MILLISECONDS.convert(5L, TimeUnit.SECONDS));
             } catch (InterruptedException e) {
                 Log.d(TAG, "InterruptedException: " + e);
             }
-        } finally {
+
+            final String fileName = UserHistoryPredictionDictionary.NAME + "." + testFilenameSuffix
+                    + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+            dictFile = new File(getContext().getFilesDir(), fileName);
+
             if (dictFile != null) {
+                assertTrue(dictFile.exists());
+                assertTrue(dictFile.length() >= MIN_USER_HISTORY_DICTIONARY_FILE_SIZE);
                 dictFile.delete();
             }
         }
@@ -122,49 +126,45 @@
 
     public void testStressTestForSwitchingLanguagesAndAddingWords() {
         final int numberOfLanguages = 2;
-        final int numberOfLanguageSwitching = 100;
-        final int numberOfWordsIntertedForEachLanguageSwitch = 100;
+        final int numberOfLanguageSwitching = 80;
+        final int numberOfWordsInsertedForEachLanguageSwitch = 100;
 
         final File dictFiles[] = new File[numberOfLanguages];
         try {
             final Random random = new Random(123456);
 
-            // Create locales for this test.
-            String locales[] = new String[numberOfLanguages];
+            // Create filename suffixes for this test.
+            String testFilenameSuffixes[] = new String[numberOfLanguages];
             for (int i = 0; i < numberOfLanguages; i++) {
-                locales[i] = "testSwitchingLanguages" + i;
-                final String fileName = "UserHistoryDictionary." + locales[i] + ".dict";
+                testFilenameSuffixes[i] = "testSwitchingLanguages" + i;
+                final String fileName = UserHistoryPredictionDictionary.NAME + "." +
+                        testFilenameSuffixes[i] + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
                 dictFiles[i] = new File(getContext().getFilesDir(), fileName);
             }
 
-            final long now = System.currentTimeMillis();
+            final long start = System.currentTimeMillis();
 
             for (int i = 0; i < numberOfLanguageSwitching; i++) {
                 final int index = i % numberOfLanguages;
-                // Switch languages to locales[index].
-                final UserHistoryPredictionDictionary dict =
-                        PersonalizationDictionaryHelper.getUserHistoryPredictionDictionary(
-                                getContext(), locales[index], mPrefs);
-                final List<String> words = generateWords(
-                        numberOfWordsIntertedForEachLanguageSwitch, random);
-                // Add random words to the user history dictionary.
-                addToDict(dict, words);
-                // write to file
-                dict.close();
+                // Switch languages to testFilenameSuffixes[index].
+                addAndWriteRandomWords(testFilenameSuffixes[index],
+                        numberOfWordsInsertedForEachLanguageSwitch, random);
             }
 
             final long end = System.currentTimeMillis();
             Log.d(TAG, "testStressTestForSwitchingLanguageAndAddingWords took "
-                    + (end - now) + " ms");
+                    + (end - start) + " ms");
+        } finally {
             try {
                 Log.d(TAG, "waiting for writing ...");
-                Thread.sleep(5000);
+                Thread.sleep(TimeUnit.MILLISECONDS.convert(5L, TimeUnit.SECONDS));
             } catch (InterruptedException e) {
                 Log.d(TAG, "InterruptedException: " + e);
             }
-        } finally {
             for (final File file : dictFiles) {
                 if (file != null) {
+                    assertTrue(file.exists());
+                    assertTrue(file.length() >= MIN_USER_HISTORY_DICTIONARY_FILE_SIZE);
                     file.delete();
                 }
             }
diff --git a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
index fd55176..8f5bec8 100644
--- a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
@@ -49,6 +49,7 @@
     private static final int BIGRAM_FREQUENCY = 100;
     private static final ArrayList<String> NOT_HAVE_BIGRAM = new ArrayList<String>();
     private static final FormatSpec.FormatOptions FORMAT_OPTIONS = new FormatSpec.FormatOptions(2);
+    private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
 
     /**
      * Return same frequency for all words and bigrams
@@ -177,7 +178,8 @@
 
         File file = null;
         try {
-            file = File.createTempFile("testReadAndWrite", ".dict", getContext().getCacheDir());
+            file = File.createTempFile("testReadAndWrite", TEST_DICT_FILE_EXTENSION,
+                    getContext().getCacheDir());
         } catch (IOException e) {
             Log.d(TAG, "IOException while creating a temporary file", e);
         }