Merge "Refactor touch event forwarding in InputView"
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index a269d49..f4d072d 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -87,6 +87,11 @@
     <!-- Description for option enabling or disabling the use of names of people in Contacts for suggestion and correction [CHAR LIMIT=65] -->
     <string name="use_contacts_dict_summary">Use names from Contacts for suggestions and corrections</string>
 
+    <!-- Option name for enabling the use by the keyboards of sent/received messages, e-mail and typing history to improve suggestion accuracy [CHAR LIMIT=25] -->
+    <string name="use_personalized_dicts">Personalized suggestions</string>
+    <!-- Description for option enabling the use by the keyboards of sent/received messages, e-mail and typing history to improve suggestion accuracy [CHAR LIMIT=65] -->
+    <string name="use_personalized_dicts_summary">Learn from your communications and typed data to improve suggestions</string>
+
     <!-- Option name for enabling or disabling the double-space period feature that lets double tap on spacebar insert a period followed by a space [CHAR LIMIT=30] -->
     <string name="use_double_space_period">Double-space period</string>
     <!-- Description for option enabling or disabling the double-space period feature that lets double tap on spacebar insert a period followed by a space [CHAR LIMIT=65] -->
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index bf3b623..4f7c9f0 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -134,6 +134,12 @@
                 android:persistent="true"
                 android:defaultValue="true" />
             <CheckBoxPreference
+                android:key="pref_key_use_personalized_dicts"
+                android:title="@string/use_personalized_dicts"
+                android:summary="@string/use_personalized_dicts_summary"
+                android:persistent="true"
+                android:defaultValue="true" />
+            <CheckBoxPreference
                 android:key="pref_key_use_double_space_period"
                 android:title="@string/use_double_space_period"
                 android:summary="@string/use_double_space_period_summary"
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 1400e05..4b6e12f 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -932,11 +932,7 @@
     @Override
     public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
         locatePreviewPlacerView();
-        // TODO: Remove this check
-        if (panel.isShowingInParent()) {
-            panel.dismissMoreKeysPanel();
-        }
-        mPreviewPlacerView.addView(panel.getContainerView());
+        panel.showInParent(mPreviewPlacerView);
         mMoreKeysPanel = panel;
         dimEntireKeyboard(true /* dimmed */);
     }
@@ -954,7 +950,7 @@
     public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
         dimEntireKeyboard(false /* dimmed */);
         if (isShowingMoreKeysPanel()) {
-            mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView());
+            mMoreKeysPanel.removeFromParent();
             mMoreKeysPanel = null;
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index a7c4685..5b13e9a 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -21,6 +21,7 @@
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
@@ -216,12 +217,26 @@
         return true;
     }
 
-    @Override
-    public View getContainerView() {
+    private View getContainerView() {
         return (View)getParent();
     }
 
     @Override
+    public void showInParent(final ViewGroup parentView) {
+        removeFromParent();
+        parentView.addView(getContainerView());
+    }
+
+    @Override
+    public void removeFromParent() {
+        final View containerView = getContainerView();
+        final ViewGroup currentParent = (ViewGroup)containerView.getParent();
+        if (currentParent != null) {
+            currentParent.removeView(containerView);
+        }
+    }
+
+    @Override
     public boolean isShowingInParent() {
         return (getContainerView().getParent() != null);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
index 886c628..4a33e65 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard;
 
 import android.view.View;
+import android.view.ViewGroup;
 
 public interface MoreKeysPanel {
     public interface Controller {
@@ -119,9 +120,16 @@
     public int translateY(int y);
 
     /**
-     * Return the view containing the more keys panel.
+     * Show this {@link MoreKeysPanel} in the parent view.
+     *
+     * @param parentView the {@link ViewGroup} that hosts this {@link MoreKeysPanel}.
      */
-    public View getContainerView();
+    public void showInParent(ViewGroup parentView);
+
+    /**
+     * Remove this {@link MoreKeysPanel} from the parent view.
+     */
+    public void removeFromParent();
 
     /**
      * Return whether the panel is currently being shown.
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 7afd657..f785835 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -140,7 +140,7 @@
         // only, which right now use format 2 (dicts using format 4 use Decaying*, which overrides
         // this method).
         // TODO: Migrate these dicts to ver4 format, and remove this function.
-        return formatVersion == 2;
+        return formatVersion == FormatSpec.VERSION2;
     }
 
     public boolean isValidDictionary() {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 8b46655..72d5435 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1469,7 +1469,7 @@
                 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
                         false /* isBatchMode */);
             }
-            mWordComposer.doubleSpacePeriod();
+            mWordComposer.discardPreviousWordForSuggestion();
             mKeyboardSwitcher.updateShiftState();
             return true;
         }
@@ -2565,29 +2565,23 @@
         final SettingsValues currentSettings = mSettings.getCurrent();
         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
 
-        final String previousWord;
-        if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) {
-            previousWord = mWordComposer.getPreviousWord();
-        } else {
-            // Not composing: this is for prediction.
-            // TODO: read the previous word earlier for prediction, like we are doing for
-            // normal suggestions.
-            previousWord = getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord*/);
-        }
         if (DEBUG) {
-            // TODO: this is for checking consistency with older versions. Remove this when
-            // we are confident this is stable.
-            // We're checking the previous word in the text field against the memorized previous
-            // word. If we are composing a word we should have the second word before the cursor
-            // memorized, otherwise we should have the first.
-            final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings,
-                    mWordComposer.isComposingWord() ? 2 : 1);
-            if (!TextUtils.equals(previousWord, rereadPrevWord)) {
-                throw new RuntimeException("Unexpected previous word: "
-                        + previousWord + " <> " + rereadPrevWord);
+            if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) {
+                final String previousWord = mWordComposer.getPreviousWordForSuggestion();
+                // TODO: this is for checking consistency with older versions. Remove this when
+                // we are confident this is stable.
+                // We're checking the previous word in the text field against the memorized previous
+                // word. If we are composing a word we should have the second word before the cursor
+                // memorized, otherwise we should have the first.
+                final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings,
+                        mWordComposer.isComposingWord() ? 2 : 1);
+                if (!TextUtils.equals(previousWord, rereadPrevWord)) {
+                    throw new RuntimeException("Unexpected previous word: "
+                            + previousWord + " <> " + rereadPrevWord);
+                }
             }
         }
-        suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWord(),
+        suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWordForSuggestion(),
                 keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
@@ -2830,6 +2824,19 @@
         // strings.
         mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
                 prevWord);
+        final boolean shouldDiscardPreviousWordForSuggestion;
+        if (0 == StringUtils.codePointCount(separatorString)) {
+            // Separator is 0-length. Discard the word only if the current language has spaces.
+            shouldDiscardPreviousWordForSuggestion =
+                    mSettings.getCurrent().mCurrentLanguageHasSpaces;
+        } else {
+            // Otherwise, we discard if the separator contains any non-whitespace.
+            shouldDiscardPreviousWordForSuggestion =
+                    !StringUtils.containsOnlyWhitespace(separatorString);
+        }
+        if (shouldDiscardPreviousWordForSuggestion) {
+            mWordComposer.discardPreviousWordForSuggestion();
+        }
     }
 
     private void setPunctuationSuggestions() {
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 5ecfc67..7da97e5 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -50,8 +50,9 @@
     private final StringBuilder mTypedWord;
     // The previous word (before the composing word). Used as context for suggestions. May be null
     // after resetting and before starting a new composing word, or when there is no context like
-    // at the start of text for example.
-    private String mPreviousWord;
+    // at the start of text for example. It can also be set to null externally when the user
+    // enters a separator that does not let bigrams across, like a period or a comma.
+    private String mPreviousWordForSuggestion;
     private String mAutoCorrection;
     private boolean mIsResumed;
     private boolean mIsBatchMode;
@@ -89,7 +90,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
-        mPreviousWord = null;
+        mPreviousWordForSuggestion = null;
         refreshSize();
     }
 
@@ -106,7 +107,7 @@
         mIsBatchMode = source.mIsBatchMode;
         mCursorPositionWithinWord = source.mCursorPositionWithinWord;
         mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
-        mPreviousWord = source.mPreviousWord;
+        mPreviousWordForSuggestion = source.mPreviousWordForSuggestion;
         refreshSize();
     }
 
@@ -124,7 +125,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
-        mPreviousWord = null;
+        mPreviousWordForSuggestion = null;
         refreshSize();
     }
 
@@ -305,7 +306,7 @@
             addKeyInfo(codePoint, keyboard);
         }
         mIsResumed = true;
-        mPreviousWord = previousWord;
+        mPreviousWordForSuggestion = previousWord;
     }
 
     /**
@@ -356,8 +357,8 @@
         return mTypedWord.toString();
     }
 
-    public String getPreviousWord() {
-        return mPreviousWord;
+    public String getPreviousWordForSuggestion() {
+        return mPreviousWordForSuggestion;
     }
 
     /**
@@ -419,7 +420,7 @@
     public void setCapitalizedModeAndPreviousWordAtStartComposingTime(final int mode,
             final String previousWord) {
         mCapitalizedMode = mode;
-        mPreviousWord = previousWord;
+        mPreviousWordForSuggestion = previousWord;
     }
 
     /**
@@ -471,12 +472,7 @@
         mCapsCount = 0;
         mDigitsCount = 0;
         mIsBatchMode = false;
-        final boolean isWhitespace = 1 == StringUtils.codePointCount(separatorString)
-                && Character.isWhitespace(separatorString.codePointAt(0));
-        // If not whitespace, we don't use the previous word for suggestion. This is consistent
-        // with how we get the previous word for suggestion: see RichInputConnection#spaceRegex and
-        // LatinIME#getNthPreviousWordForSuggestion.
-        mPreviousWord = isWhitespace ? mTypedWord.toString() : null;
+        mPreviousWordForSuggestion = mTypedWord.toString();
         mTypedWord.setLength(0);
         mCodePointSize = 0;
         mTrailingSingleQuotesCount = 0;
@@ -490,11 +486,11 @@
         return lastComposedWord;
     }
 
-    public void doubleSpacePeriod() {
-        // When a period was entered with a double space, the separator we got has been
-        // changed by a period (see #commitWord). We should not use the previous word for
-        // suggestion.
-        mPreviousWord = null;
+    // Call this when the recorded previous word should be discarded. This is typically called
+    // when the user inputs a separator that's not whitespace (including the case of the
+    // double-space-to-period feature).
+    public void discardPreviousWordForSuggestion() {
+        mPreviousWordForSuggestion = null;
     }
 
     public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord,
@@ -509,7 +505,7 @@
         mCursorPositionWithinWord = mCodePointSize;
         mRejectedBatchModeSuggestion = null;
         mIsResumed = true;
-        mPreviousWord = previousWord;
+        mPreviousWordForSuggestion = previousWord;
     }
 
     public boolean isBatchMode() {
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index 7f0aa77..acabea1 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -436,7 +436,7 @@
             final FormatOptions options) {
         dictDecoder.setPosition(headerSize);
         final int count = dictDecoder.readPtNodeCount();
-        int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count);
+        int groupPos = dictDecoder.getPosition();
         final StringBuilder builder = new StringBuilder();
         WeightedString result = null;
 
@@ -498,9 +498,9 @@
         do { // Scan the linked-list node.
             final int nodeArrayHeadPos = dictDecoder.getPosition();
             final int count = dictDecoder.readPtNodeCount();
-            int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count);
+            int groupPos = dictDecoder.getPosition();
             for (int i = count; i > 0; --i) { // Scan the array of PtNode.
-                PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options);
+                PtNodeInfo info = dictDecoder.readPtNode(groupPos, options);
                 if (BinaryDictIOUtils.isMovedPtNode(info.mFlags, options)) continue;
                 ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
                 ArrayList<WeightedString> bigrams = null;
@@ -536,7 +536,7 @@
                                     0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
                                     0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
                 }
-                groupOffsetPos = info.mEndAddress;
+                groupPos = info.mEndAddress;
             }
 
             // reach the end of the array.
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index 640d778..efe1b7b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -87,7 +87,7 @@
 
             if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) {
                 p.mNumOfPtNode = dictDecoder.readPtNodeCount();
-                p.mAddress += getPtNodeCountSize(p.mNumOfPtNode);
+                p.mAddress = dictDecoder.getPosition();
                 p.mPosition = 0;
             }
             if (p.mNumOfPtNode == 0) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 2f6f194..437fa94 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -188,7 +188,7 @@
     // us to change the format during development while having testing devices remove
     // older files with each upgrade, while still having a readable versioning scheme.
     public static final int VERSION2 = 2;
-    public static final int VERSION4 = 400;
+    public static final int VERSION4 = 401;
     static final int MINIMUM_SUPPORTED_VERSION = VERSION2;
     static final int MAXIMUM_SUPPORTED_VERSION = VERSION4;
 
@@ -351,18 +351,19 @@
         public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary";
         private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description";
         public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
-                final FormatOptions formatOptions) {
+                final FormatOptions formatOptions) throws UnsupportedFormatException {
             mDictionaryOptions = dictionaryOptions;
             mFormatOptions = formatOptions;
             mBodyOffset = formatOptions.mVersion < VERSION4 ? headerSize : 0;
             if (null == getLocaleString()) {
-                throw new RuntimeException("Cannot create a FileHeader without a locale");
+                throw new UnsupportedFormatException("Cannot create a FileHeader without a locale");
             }
             if (null == getVersion()) {
-                throw new RuntimeException("Cannot create a FileHeader without a version");
+                throw new UnsupportedFormatException(
+                        "Cannot create a FileHeader without a version");
             }
             if (null == getId()) {
-                throw new RuntimeException("Cannot create a FileHeader without an ID");
+                throw new UnsupportedFormatException("Cannot create a FileHeader without an ID");
             }
         }
 
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 9b573b4..3a5c115 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -53,7 +53,7 @@
     public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED;
     public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY;
 
-    public static final int REQUIRED_BINARY_DICTIONARY_VERSION = 4;
+    public static final int REQUIRED_BINARY_DICTIONARY_VERSION = FormatSpec.VERSION4;
 
     /** Locale for which this user history dictionary is storing words */
     private final Locale mLocale;
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 714c3a9..94f1458 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -53,6 +53,7 @@
     public static final String PREF_MISC_SETTINGS = "misc_settings";
     public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
     public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+    public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts";
     public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD =
             "pref_key_use_double_space_period";
     public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE =
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 06406c1..d98e547 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -29,11 +29,9 @@
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.utils.CollectionUtils;
-import com.android.inputmethod.latin.utils.InputTypeUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.util.ArrayList;
@@ -71,6 +69,7 @@
     public final boolean mIncludesOtherImesInLanguageSwitchList;
     public final boolean mShowsLanguageSwitchKey;
     public final boolean mUseContactsDict;
+    public final boolean mUsePersonalizedDicts;
     public final boolean mUseDoubleSpacePeriod;
     public final boolean mBlockPotentiallyOffensive;
     // Use bigrams to predict the next word when there is no input for it yet
@@ -148,6 +147,7 @@
                 Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false);
         mShowsLanguageSwitchKey = Settings.readShowsLanguageSwitchKey(prefs);
         mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
+        mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true);
         mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true);
         mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res);
         mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res);
@@ -206,6 +206,7 @@
         mIncludesOtherImesInLanguageSwitchList = false;
         mShowsLanguageSwitchKey = true;
         mUseContactsDict = true;
+        mUsePersonalizedDicts = true;
         mUseDoubleSpacePeriod = true;
         mBlockPotentiallyOffensive = true;
         mAutoCorrectEnabled = true;
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index df42041..85f4454 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -250,6 +250,24 @@
         return true;
     }
 
+    /**
+     * Returns true if all code points in text are whitespace, false otherwise. Empty is true.
+     */
+    // Interestingly enough, U+00A0 NO-BREAK SPACE and U+200B ZERO-WIDTH SPACE are not considered
+    // whitespace, while EN SPACE, EM SPACE and IDEOGRAPHIC SPACES are.
+    public static boolean containsOnlyWhitespace(final String text) {
+        final int length = text.length();
+        int i = 0;
+        while (i < length) {
+            final int codePoint = text.codePointAt(i);
+            if (!Character.isWhitespace(codePoint)) {
+                return false;
+            }
+            i += Character.charCount(codePoint);
+        }
+        return true;
+    }
+
     @UsedForTesting
     public static boolean looksValidForDictionaryInsertion(final CharSequence text,
             final SettingsValues settings) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
index 9afb5f2..deed010 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
@@ -48,9 +48,9 @@
 const int Ver4DictConstants::WORD_LEVEL_FIELD_SIZE = 1;
 const int Ver4DictConstants::WORD_COUNT_FIELD_SIZE = 1;
 
-const int Ver4DictConstants::BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4;
+const int Ver4DictConstants::BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16;
 const int Ver4DictConstants::BIGRAM_ADDRESS_TABLE_DATA_SIZE = 4;
-const int Ver4DictConstants::SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 16;
+const int Ver4DictConstants::SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64;
 const int Ver4DictConstants::SHORTCUT_ADDRESS_TABLE_DATA_SIZE = 4;
 
 const int Ver4DictConstants::BIGRAM_TARGET_TERMINAL_ID_FIELD_SIZE = 3;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
index eb2227d..7c6a21d 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
@@ -31,7 +31,7 @@
     enum FORMAT_VERSION {
         // These MUST have the same values as the relevant constants in FormatSpec.java.
         VERSION_2 = 2,
-        VERSION_4 = 400,
+        VERSION_4 = 401,
         UNKNOWN_VERSION = -1
     };
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/sparse_table.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/sparse_table.cpp
index 4ad82f9..c380429 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/sparse_table.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/sparse_table.cpp
@@ -34,7 +34,8 @@
     const int indexTableReadingPos = getPosInIndexTable(id);
     const int index = mIndexTableBuffer->readUint(INDEX_SIZE, indexTableReadingPos);
     const int contentTableReadingPos = getPosInContentTable(id, index);
-    return mContentTableBuffer->readUint(mDataSize, contentTableReadingPos);
+    const int contentValue = mContentTableBuffer->readUint(mDataSize, contentTableReadingPos);
+    return contentValue == NOT_EXIST ? NOT_A_DICT_POS : contentValue;
 }
 
 bool SparseTable::set(const int id, const uint32_t value) {
@@ -70,7 +71,7 @@
     // Write a new block that containing the entry to be set.
     int writingPos = getPosInContentTable(0 /* id */, index);
     for (int i = 0; i < mBlockSize; ++i) {
-        if (!mContentTableBuffer->writeUintAndAdvancePosition(NOT_A_DICT_POS, mDataSize,
+        if (!mContentTableBuffer->writeUintAndAdvancePosition(NOT_EXIST, mDataSize,
                 &writingPos)) {
             AKLOGE("cannot write content table to extend. writingPos: %d, tailPos: %d, "
                     "mDataSize: %d", writingPos, mContentTableBuffer->getTailPosition(), mDataSize);
diff --git a/tests/src/com/android/inputmethod/latin/WordComposerTests.java b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
index a67f6a4..1336c6d 100644
--- a/tests/src/com/android/inputmethod/latin/WordComposerTests.java
+++ b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
@@ -51,14 +51,14 @@
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
         // Check the previous word is still there
-        assertEquals(PREVWORD, wc.getPreviousWord());
+        assertEquals(PREVWORD, wc.getPreviousWordForSuggestion());
         // Move the cursor past the end of the word
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(15));
         // Do what LatinIME does when the cursor is moved outside of the word,
         // and check the behavior is correct.
         wc.reset();
-        assertNull(wc.getPreviousWord());
+        assertNull(wc.getPreviousWordForSuggestion());
 
         // \uD861\uDED7 is 𨛗, a character outside the BMP
         final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh";
@@ -73,35 +73,35 @@
         assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
-        assertNull(wc.getPreviousWord());
+        assertNull(wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
-        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR,
                 null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
-        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-3));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-1));
-        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
                 null /* keyboard */);
         wc.setCursorPositionWithinWord(3);
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-9));
-        assertNull(wc.getPreviousWord());
+        assertNull(wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR,
                 null /* keyboard */);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-10));
-        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWordForSuggestion());
 
         wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null /* previousWord */,
                 null /* keyboard */);
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
index 2123e84..0c88f34 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
@@ -292,6 +292,20 @@
         assertTrue(bytesStr.equals(bytesStr2));
     }
 
+    public void testContainsOnlyWhitespace() {
+        assertTrue(StringUtils.containsOnlyWhitespace("   "));
+        assertTrue(StringUtils.containsOnlyWhitespace(""));
+        assertTrue(StringUtils.containsOnlyWhitespace("  \n\t\t"));
+        // U+2002 : EN SPACE
+        // U+2003 : EM SPACE
+        // U+3000 : IDEOGRAPHIC SPACE (commonly "double-width space")
+        assertTrue(StringUtils.containsOnlyWhitespace("\u2002\u2003\u3000"));
+        assertFalse(StringUtils.containsOnlyWhitespace("  a "));
+        assertFalse(StringUtils.containsOnlyWhitespace(". "));
+        assertFalse(StringUtils.containsOnlyWhitespace("."));
+        assertTrue(StringUtils.containsOnlyWhitespace(""));
+    }
+
     public void testJsonUtils() {
         final Object[] objs = new Object[] { 1, "aaa", "bbb", 3 };
         final List<Object> objArray = Arrays.asList(objs);
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index 9e397f1..0c11f86 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -42,9 +42,16 @@
     private static final int TEST_FREQ = 37; // Some arbitrary value unlikely to happen by chance
 
     public void testGetRawDictWorks() throws IOException, UnsupportedFormatException {
+        final String VERSION = "1";
+        final String LOCALE = "test";
+        final String ID = "main:test";
+
         // Create a thrice-compressed dictionary file.
-        final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                new DictionaryOptions(new HashMap<String, String>()));
+        final DictionaryOptions testOptions = new DictionaryOptions(new HashMap<String, String>());
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE, VERSION);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, LOCALE);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, ID);
+        final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), testOptions);
         dict.add("foo", TEST_FREQ, null, false /* isNotAWord */);
         dict.add("fta", 1, null, false /* isNotAWord */);
         dict.add("ftb", 1, null, false /* isNotAWord */);
@@ -72,6 +79,12 @@
         final FusionDictionary resultDict = dictDecoder.readDictionaryBinary(
                 null /* dict : an optional dictionary to add words to, or null */,
                 false /* deleteDictIfBroken */);
+        assertEquals("Wrong version attribute", VERSION, resultDict.mOptions.mAttributes.get(
+                FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE));
+        assertEquals("Wrong locale attribute", LOCALE, resultDict.mOptions.mAttributes.get(
+                FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE));
+        assertEquals("Wrong id attribute", ID, resultDict.mOptions.mAttributes.get(
+                FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE));
         assertEquals("Dictionary can't be read back correctly",
                 FusionDictionary.findWordInTree(resultDict.mRootNodeArray, "foo").getFrequency(),
                 TEST_FREQ);