Merge "[CS4] Remove useless tests and processing"
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 335e52f..95e1340 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -174,7 +174,7 @@
     private static native int getFormatVersionNative(long dict);
     private static native int getProbabilityNative(long dict, int[] word);
     private static native int getMaxProbabilityOfExactMatchesNative(long dict, int[] word);
-    private static native int getBigramProbabilityNative(long dict, int[] word0,
+    private static native int getNgramProbabilityNative(long dict, int[] word0,
             boolean isBeginningOfSentence, int[] word1);
     private static native void getWordPropertyNative(long dict, int[] word,
             boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags,
@@ -190,13 +190,13 @@
             int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores,
             int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence,
             float[] inOutLanguageWeight);
-    private static native boolean addUnigramWordNative(long dict, int[] word, int probability,
+    private static native boolean addUnigramEntryNative(long dict, int[] word, int probability,
             int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence,
             boolean isNotAWord, boolean isBlacklisted, int timestamp);
-    private static native boolean removeUnigramWordNative(long dict, int[] word);
-    private static native boolean addBigramWordsNative(long dict, int[] word0,
+    private static native boolean removeUnigramEntryNative(long dict, int[] word);
+    private static native boolean addNgramEntryNative(long dict, int[] word0,
             boolean isBeginningOfSentence, int[] word1, int probability, int timestamp);
-    private static native boolean removeBigramWordsNative(long dict, int[] word0,
+    private static native boolean removeNgramEntryNative(long dict, int[] word0,
             boolean isBeginningOfSentence, int[] word1);
     private static native int addMultipleDictionaryEntriesNative(long dict,
             LanguageModelParam[] languageModelParams, int startIndex);
@@ -262,9 +262,8 @@
         }
         final DicTraverseSession session = getTraverseSession(sessionId);
         Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
-        // TODO: toLowerCase in the native code
-        final int[] prevWordCodePointArray = (null == prevWordsInfo.mPrevWord)
-                ? null : StringUtils.toCodePointArray(prevWordsInfo.mPrevWord);
+        prevWordsInfo.outputToArray(session.mPrevWordCodePointArrays,
+                session.mIsBeginningOfSentenceArray);
         final InputPointers inputPointers = composer.getInputPointers();
         final boolean isGesture = composer.isBatchMode();
         final int inputSize;
@@ -286,13 +285,13 @@
         } else {
             session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
         }
-        // proximityInfo and/or prevWordForBigrams may not be null.
+        // TOOD: Pass multiple previous words information for n-gram.
         getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
                 getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
                 inputPointers.getYCoordinates(), inputPointers.getTimes(),
                 inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
-                session.mNativeSuggestOptions.getOptions(), prevWordCodePointArray,
-                prevWordsInfo.mIsBeginningOfSentence, session.mOutputSuggestionCount,
+                session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays[0],
+                session.mIsBeginningOfSentenceArray[0], session.mOutputSuggestionCount,
                 session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices,
                 session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence,
                 session.mInputOutputLanguageWeight);
@@ -355,10 +354,13 @@
         if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
             return NOT_A_PROBABILITY;
         }
-        final int[] codePoints0 = StringUtils.toCodePointArray(prevWordsInfo.mPrevWord);
-        final int[] codePoints1 = StringUtils.toCodePointArray(word);
-        return getBigramProbabilityNative(mNativeDict, codePoints0,
-                prevWordsInfo.mIsBeginningOfSentence, codePoints1);
+        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
+        final boolean[] isBeginningOfSentenceArray =
+                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+        return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays[0],
+                isBeginningOfSentenceArray[0], wordCodePoints);
     }
 
     public WordProperty getWordProperty(final String word, final boolean isBeginningOfSentence) {
@@ -422,7 +424,7 @@
         final int[] codePoints = StringUtils.toCodePointArray(word);
         final int[] shortcutTargetCodePoints = (shortcutTarget != null) ?
                 StringUtils.toCodePointArray(shortcutTarget) : null;
-        if (!addUnigramWordNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
+        if (!addUnigramEntryNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints,
                 shortcutProbability, isBeginningOfSentence, isNotAWord, isBlacklisted, timestamp)) {
             return false;
         }
@@ -436,7 +438,7 @@
             return false;
         }
         final int[] codePoints = StringUtils.toCodePointArray(word);
-        if (!removeUnigramWordNative(mNativeDict, codePoints)) {
+        if (!removeUnigramEntryNative(mNativeDict, codePoints)) {
             return false;
         }
         mHasUpdated = true;
@@ -449,10 +451,13 @@
         if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
             return false;
         }
-        final int[] codePoints0 = StringUtils.toCodePointArray(prevWordsInfo.mPrevWord);
-        final int[] codePoints1 = StringUtils.toCodePointArray(word);
-        if (!addBigramWordsNative(mNativeDict, codePoints0, prevWordsInfo.mIsBeginningOfSentence,
-                codePoints1, probability, timestamp)) {
+        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
+        final boolean[] isBeginningOfSentenceArray =
+                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+        if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays[0],
+                isBeginningOfSentenceArray[0], wordCodePoints, probability, timestamp)) {
             return false;
         }
         mHasUpdated = true;
@@ -464,10 +469,13 @@
         if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) {
             return false;
         }
-        final int[] codePoints0 = StringUtils.toCodePointArray(prevWordsInfo.mPrevWord);
-        final int[] codePoints1 = StringUtils.toCodePointArray(word);
-        if (!removeBigramWordsNative(mNativeDict, codePoints0, prevWordsInfo.mIsBeginningOfSentence,
-                codePoints1)) {
+        final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
+        final boolean[] isBeginningOfSentenceArray =
+                new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+        prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray);
+        final int[] wordCodePoints = StringUtils.toCodePointArray(word);
+        if (!removeNgramEntryNative(mNativeDict, prevWordCodePointArrays[0],
+                isBeginningOfSentenceArray[0], wordCodePoints)) {
             return false;
         }
         mHasUpdated = true;
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index f933936..b4e115c 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -166,6 +166,10 @@
     // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
     public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
 
+    // (MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1)-gram is supported in Java side. Needs to modify
+    // MAX_PREV_WORD_COUNT_FOR_N_GRAM in native/jni/src/defines.h for suggestions.
+    public static final int MAX_PREV_WORD_COUNT_FOR_N_GRAM = 2;
+
     // Key events coming any faster than this are long-presses.
     public static final int LONG_PRESS_MILLISECONDS = 200;
     // TODO: Set this value appropriately.
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 96160fa..ad14c06 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -233,19 +233,19 @@
                 final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
                     if (DEBUG) {
-                        Log.d(TAG, "addName " + name + ", " + word + ", "
-                                + prevWordsInfo.mPrevWord);
+                        Log.d(TAG, "addName " + name + ", " + word + ", "  + prevWordsInfo);
                     }
                     runGCIfRequiredLocked(true /* mindsBlockByGC */);
                     addUnigramLocked(word, FREQUENCY_FOR_CONTACTS,
                             null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */,
                             false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
-                    if (!TextUtils.isEmpty(prevWordsInfo.mPrevWord) && mUseFirstLastBigrams) {
+                    if (!prevWordsInfo.isValid() && mUseFirstLastBigrams) {
                         runGCIfRequiredLocked(true /* mindsBlockByGC */);
                         addNgramEntryLocked(prevWordsInfo, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
                                 BinaryDictionary.NOT_A_VALID_TIMESTAMP);
                     }
-                    prevWordsInfo = new PrevWordsInfo(word);
+                    prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(
+                            new PrevWordsInfo.WordInfo(word));
                 }
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index 8bbf426..b341f62 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -28,6 +28,10 @@
     // Must be equal to MAX_RESULTS in native/jni/src/defines.h
     private static final int MAX_RESULTS = 18;
     public final int[] mInputCodePoints = new int[Constants.DICTIONARY_MAX_WORD_LENGTH];
+    public final int[][] mPrevWordCodePointArrays =
+            new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][];
+    public final boolean[] mIsBeginningOfSentenceArray =
+            new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
     public final int[] mOutputSuggestionCount = new int[1];
     public final int[] mOutputCodePoints =
             new int[Constants.DICTIONARY_MAX_WORD_LENGTH * MAX_RESULTS];
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index 304c450..b8feb22 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -23,6 +23,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.personalization.ContextualDictionary;
 import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
@@ -407,13 +408,14 @@
             final boolean blockPotentiallyOffensive) {
         final Dictionaries dictionaries = mDictionaries;
         final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
+        PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo;
         for (int i = 0; i < words.length; i++) {
             final String currentWord = words[i];
-            final PrevWordsInfo prevWordsInfoForCurrentWord =
-                    (i == 0) ? prevWordsInfo : new PrevWordsInfo(words[i - 1]);
             final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false;
             addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord,
                     wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive);
+            prevWordsInfoForCurrentWord =
+                    prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord));
         }
     }
 
@@ -639,7 +641,8 @@
                 contextualDict.addNgramEntry(prevWordsInfo, phrase[i],
                         bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
             }
-            prevWordsInfo = new PrevWordsInfo(phrase[i]);
+            prevWordsInfo =
+                    prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.WordInfo(phrase[i]));
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
index 42b311c..5dda444 100644
--- a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
+++ b/java/src/com/android/inputmethod/latin/PrevWordsInfo.java
@@ -16,47 +16,122 @@
 
 package com.android.inputmethod.latin;
 
+import java.util.Arrays;
+
+import com.android.inputmethod.latin.utils.StringUtils;
+
 /**
  * Class to represent information of previous words. This class is used to add n-gram entries
  * into binary dictionaries, to get predictions, and to get suggestions.
  */
-// TODO: Support multiple previous words for n-gram.
 public class PrevWordsInfo {
-    public static final PrevWordsInfo EMPTY_PREV_WORDS_INFO = new PrevWordsInfo(null);
+    public static final PrevWordsInfo EMPTY_PREV_WORDS_INFO =
+            new PrevWordsInfo(WordInfo.EMPTY_WORD_INFO);
     public static final PrevWordsInfo BEGINNING_OF_SENTENCE = new PrevWordsInfo();
 
-    // The word immediately before the considered word. null means we don't have any context
-    // including the "beginning of sentence context" - we just don't know what to predict.
-    // An example of that is after a comma.
-    // For simplicity of implementation, this may also be null transiently after the WordComposer
-    // was reset and before starting a new composing word, but we should never be calling
-    // getSuggetions* in this situation.
-    // This is an empty string when mIsBeginningOfSentence is true.
-    public final String mPrevWord;
+    /**
+     * Word information used to represent previous words information.
+     */
+    public static class WordInfo {
+        public static final WordInfo EMPTY_WORD_INFO = new WordInfo(null);
+        public static final WordInfo BEGINNING_OF_SENTENCE = new WordInfo();
 
-    // TODO: Have sentence separator.
-    // Whether the current context is beginning of sentence or not. This is true when composing at
-    // the beginning of an input field or composing a word after a sentence separator.
-    public final boolean mIsBeginningOfSentence;
+        // This is an empty string when mIsBeginningOfSentence is true.
+        public final String mWord;
+        // TODO: Have sentence separator.
+        // Whether the current context is beginning of sentence or not. This is true when composing
+        // at the beginning of an input field or composing a word after a sentence separator.
+        public final boolean mIsBeginningOfSentence;
+
+        // Beginning of sentence.
+        public WordInfo() {
+            mWord = "";
+            mIsBeginningOfSentence = true;
+        }
+
+        public WordInfo(final String word) {
+            mWord = word;
+            mIsBeginningOfSentence = false;
+        }
+
+        public boolean isValid() {
+            return mWord != null;
+        }
+    }
+
+    // The words immediately before the considered word. EMPTY_WORD_INFO element means we don't
+    // have any context for that previous word including the "beginning of sentence context" - we
+    // just don't know what to predict using the information. An example of that is after a comma.
+    // For simplicity of implementation, elements may also be EMPTY_WORD_INFO transiently after the
+    // WordComposer was reset and before starting a new composing word, but we should never be
+    // calling getSuggetions* in this situation.
+    public WordInfo[] mPrevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
 
     // Beginning of sentence.
     public PrevWordsInfo() {
-        mPrevWord = "";
-        mIsBeginningOfSentence = true;
+        mPrevWordsInfo[0] = WordInfo.BEGINNING_OF_SENTENCE;
+        Arrays.fill(mPrevWordsInfo, 1 /* start */, mPrevWordsInfo.length, WordInfo.EMPTY_WORD_INFO);
     }
 
-    public PrevWordsInfo(final String prevWord) {
-        mPrevWord = prevWord;
-        mIsBeginningOfSentence = false;
+    // Construct from the previous word information.
+    public PrevWordsInfo(final WordInfo prevWordInfo) {
+        mPrevWordsInfo[0] = prevWordInfo;
+        Arrays.fill(mPrevWordsInfo, 1 /* start */, mPrevWordsInfo.length, WordInfo.EMPTY_WORD_INFO);
+    }
+
+    // Construct from WordInfo array. n-th element represents (n+1)-th previous word's information.
+    public PrevWordsInfo(final WordInfo[] prevWordsInfo) {
+        for (int i = 0; i < Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM; i++) {
+            mPrevWordsInfo[i] =
+                    (prevWordsInfo.length > i) ? prevWordsInfo[i] : WordInfo.EMPTY_WORD_INFO;
+        }
+    }
+
+    // Create next prevWordsInfo using current prevWordsInfo.
+    public PrevWordsInfo getNextPrevWordsInfo(final WordInfo wordInfo) {
+        final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+        prevWordsInfo[0] = wordInfo;
+        for (int i = 1; i < prevWordsInfo.length; i++) {
+            prevWordsInfo[i] = mPrevWordsInfo[i - 1];
+        }
+        return new PrevWordsInfo(prevWordsInfo);
     }
 
     public boolean isValid() {
-        return mPrevWord != null;
+        return mPrevWordsInfo[0].isValid();
+    }
+
+    public void outputToArray(final int[][] codePointArrays,
+            final boolean[] isBeginningOfSentenceArray) {
+        for (int i = 0; i < mPrevWordsInfo.length; i++) {
+            final WordInfo wordInfo = mPrevWordsInfo[i];
+            if (wordInfo == null || !wordInfo.isValid()) {
+                codePointArrays[i] = new int[0];
+                isBeginningOfSentenceArray[i] = false;
+                continue;
+            }
+            codePointArrays[i] = StringUtils.toCodePointArray(wordInfo.mWord);
+            isBeginningOfSentenceArray[i] = wordInfo.mIsBeginningOfSentence;
+        }
     }
 
     @Override
     public String toString() {
-        return "PrevWord: " + mPrevWord + ", isBeginningOfSentence: "
-                    + mIsBeginningOfSentence + ".";
+        final StringBuffer builder = new StringBuffer();
+        for (int i = 0; i < mPrevWordsInfo.length; i++) {
+            final WordInfo wordInfo = mPrevWordsInfo[i];
+            builder.append("PrevWord[");
+            builder.append(i);
+            builder.append("]: ");
+            if (!wordInfo.isValid()) {
+                builder.append("Empty. ");
+                continue;
+            }
+            builder.append(wordInfo.mWord);
+            builder.append(", isBeginningOfSentence: ");
+            builder.append(wordInfo.mIsBeginningOfSentence);
+            builder.append(". ");
+        }
+        return builder.toString();
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 96476b2..3be6bcc 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -603,7 +603,7 @@
                 || spacingAndPunctuations.isWordConnector(lastChar)) {
             return PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
         }
-        return new PrevWordsInfo(nthPrevWord);
+        return new PrevWordsInfo(new PrevWordsInfo.WordInfo(nthPrevWord));
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 4d3f5b5..c9d0dcf 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -1536,7 +1536,8 @@
         } else {
             return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ?
                     PrevWordsInfo.BEGINNING_OF_SENTENCE :
-                            new PrevWordsInfo(mLastComposedWord.mCommittedWord.toString());
+                            new PrevWordsInfo(new PrevWordsInfo.WordInfo(
+                                    mLastComposedWord.mCommittedWord.toString()));
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index 3916fc2..a98b0f1 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -60,7 +60,7 @@
     public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary,
             final PrevWordsInfo prevWordsInfo, final String word, final boolean isValid,
             final int timestamp, final DistracterFilter distracterFilter) {
-        final String prevWord = prevWordsInfo.mPrevWord;
+        final String prevWord = prevWordsInfo.mPrevWordsInfo[0].mWord;
         if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH ||
                 (prevWord != null && prevWord.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) {
             return;
@@ -75,7 +75,13 @@
             return;
         }
         if (null != prevWord) {
-            userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp);
+            if (prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence) {
+                // Beginning-of-Sentence n-gram entry is treated as a n-gram entry of invalid word.
+                userHistoryDictionary.addNgramEntry(prevWordsInfo, word,
+                        FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp);
+            } else {
+                userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp);
+            }
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 8d49564..d031624 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -37,6 +37,7 @@
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.ScriptUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.lang.ref.WeakReference;
@@ -78,40 +79,8 @@
     private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
             new HashSet<>();
 
-    public static final int SCRIPT_LATIN = 0;
-    public static final int SCRIPT_CYRILLIC = 1;
-    public static final int SCRIPT_GREEK = 2;
     public static final String SINGLE_QUOTE = "\u0027";
     public static final String APOSTROPHE = "\u2019";
-    private static final TreeMap<String, Integer> mLanguageToScript;
-    static {
-        // List of the supported languages and their associated script. We won't check
-        // words written in another script than the selected script, because we know we
-        // don't have those in our dictionary so we will underline everything and we
-        // will never have any suggestions, so it makes no sense checking them, and this
-        // is done in {@link #shouldFilterOut}. Also, the script is used to choose which
-        // proximity to pass to the dictionary descent algorithm.
-        // IMPORTANT: this only contains languages - do not write countries in there.
-        // Only the language is searched from the map.
-        mLanguageToScript = new TreeMap<>();
-        mLanguageToScript.put("cs", SCRIPT_LATIN);
-        mLanguageToScript.put("da", SCRIPT_LATIN);
-        mLanguageToScript.put("de", SCRIPT_LATIN);
-        mLanguageToScript.put("el", SCRIPT_GREEK);
-        mLanguageToScript.put("en", SCRIPT_LATIN);
-        mLanguageToScript.put("es", SCRIPT_LATIN);
-        mLanguageToScript.put("fi", SCRIPT_LATIN);
-        mLanguageToScript.put("fr", SCRIPT_LATIN);
-        mLanguageToScript.put("hr", SCRIPT_LATIN);
-        mLanguageToScript.put("it", SCRIPT_LATIN);
-        mLanguageToScript.put("lt", SCRIPT_LATIN);
-        mLanguageToScript.put("lv", SCRIPT_LATIN);
-        mLanguageToScript.put("nb", SCRIPT_LATIN);
-        mLanguageToScript.put("nl", SCRIPT_LATIN);
-        mLanguageToScript.put("pt", SCRIPT_LATIN);
-        mLanguageToScript.put("sl", SCRIPT_LATIN);
-        mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
-    }
 
     @Override public void onCreate() {
         super.onCreate();
@@ -122,22 +91,13 @@
         onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
     }
 
-    public static int getScriptFromLocale(final Locale locale) {
-        final Integer script = mLanguageToScript.get(locale.getLanguage());
-        if (null == script) {
-            throw new RuntimeException("We have been called with an unsupported language: \""
-                    + locale.getLanguage() + "\". Framework bug?");
-        }
-        return script;
-    }
-
     private static String getKeyboardLayoutNameForScript(final int script) {
         switch (script) {
-        case AndroidSpellCheckerService.SCRIPT_LATIN:
+        case ScriptUtils.SCRIPT_LATIN:
             return "qwerty";
-        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+        case ScriptUtils.SCRIPT_CYRILLIC:
             return "east_slavic";
-        case AndroidSpellCheckerService.SCRIPT_GREEK:
+        case ScriptUtils.SCRIPT_GREEK:
             return "greek";
         default:
             throw new RuntimeException("Wrong script supplied: " + script);
@@ -413,7 +373,7 @@
     }
 
     public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
-        final int script = getScriptFromLocale(locale);
+        final int script = ScriptUtils.getScriptFromLocale(locale);
         final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
         final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
                 locale.toString(), keyboardLayoutName, null);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
index 55274cf..6bfd354 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -61,7 +61,8 @@
             final int offset = ssi.getOffsetAt(i);
             final int length = ssi.getLengthAt(i);
             final String subText = typedText.substring(offset, offset + length);
-            final PrevWordsInfo prevWordsInfo = new PrevWordsInfo(currentWord);
+            final PrevWordsInfo prevWordsInfo =
+                    new PrevWordsInfo(new PrevWordsInfo.WordInfo(currentWord));
             currentWord = subText;
             if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
                 continue;
@@ -203,7 +204,8 @@
                 } else {
                     prevWord = null;
                 }
-                final PrevWordsInfo prevWordsInfo = new PrevWordsInfo(prevWord);
+                final PrevWordsInfo prevWordsInfo =
+                        new PrevWordsInfo(new PrevWordsInfo.WordInfo(prevWord));
                 retval[i] = onGetSuggestionsInternal(textInfos[i], prevWordsInfo, suggestionsLimit);
                 retval[i].setCookieAndSequence(textInfos[i].getCookie(),
                         textInfos[i].getSequence());
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 54eebe3..db15c1c 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -36,6 +36,7 @@
 import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.ScriptUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
 import java.util.ArrayList;
@@ -72,10 +73,10 @@
 
         // TODO: Support n-gram input
         private static String generateKey(final String query, final PrevWordsInfo prevWordsInfo) {
-            if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWordsInfo.mPrevWord)) {
+            if (TextUtils.isEmpty(query) || !prevWordsInfo.isValid()) {
                 return query;
             }
-            return query + CHAR_DELIMITER + prevWordsInfo.mPrevWord;
+            return query + CHAR_DELIMITER + prevWordsInfo;
         }
 
         public SuggestionsParams getSuggestionsFromCache(String query,
@@ -116,7 +117,7 @@
         final String localeString = getLocale();
         mDictionaryPool = mService.getDictionaryPool(localeString);
         mLocale = LocaleUtils.constructLocaleFromString(localeString);
-        mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale);
+        mScript = ScriptUtils.getScriptFromLocale(mLocale);
     }
 
     @Override
@@ -125,44 +126,6 @@
         cres.unregisterContentObserver(mObserver);
     }
 
-    /*
-     * Returns whether the code point is a letter that makes sense for the specified
-     * locale for this spell checker.
-     * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
-     * and is limited to EFIGS languages and Russian.
-     * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
-     * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
-     */
-    private static boolean isLetterCheckableByLanguage(final int codePoint,
-            final int script) {
-        switch (script) {
-        case AndroidSpellCheckerService.SCRIPT_LATIN:
-            // Our supported latin script dictionaries (EFIGS) at the moment only include
-            // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
-            // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
-            // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
-            // excluded from isLetter anyway.
-            return codePoint <= 0x2AF && Character.isLetter(codePoint);
-        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
-            // All Cyrillic characters are in the 400~52F block. There are some in the upper
-            // Unicode range, but they are archaic characters that are not used in modern
-            // Russian and are not used by our dictionary.
-            return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
-        case AndroidSpellCheckerService.SCRIPT_GREEK:
-            // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the
-            // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters.
-            // Our dictionary also contains a few words with 0xF2; it would be best to check
-            // if that's correct, but a web search does return results for these words so
-            // they are probably okay.
-            return (codePoint >= 0x370 && codePoint <= 0x3FF)
-                    || (codePoint >= 0x1F00 && codePoint <= 0x1FFF)
-                    || codePoint == 0xF2;
-        default:
-            // Should never come here
-            throw new RuntimeException("Impossible value of script: " + script);
-        }
-    }
-
     private static final int CHECKABILITY_CHECKABLE = 0;
     private static final int CHECKABILITY_TOO_MANY_NON_LETTERS = 1;
     private static final int CHECKABILITY_CONTAINS_PERIOD = 2;
@@ -189,7 +152,7 @@
         // Filter by first letter
         final int firstCodePoint = text.codePointAt(0);
         // Filter out words that don't start with a letter or an apostrophe
-        if (!isLetterCheckableByLanguage(firstCodePoint, script)
+        if (!ScriptUtils.isLetterCheckableByScript(firstCodePoint, script)
                 && '\'' != firstCodePoint) return CHECKABILITY_FIRST_LETTER_UNCHECKABLE;
 
         // Filter contents
@@ -210,7 +173,7 @@
             if (Constants.CODE_PERIOD == codePoint) {
                 return CHECKABILITY_CONTAINS_PERIOD;
             }
-            if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
+            if (ScriptUtils.isLetterCheckableByScript(codePoint, script)) ++letterCount;
         }
         // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
         // in this word are letters
diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
index 4248beb..9ec19ef 100644
--- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
+++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
@@ -117,7 +117,8 @@
                 continue;
             }
             languageModelParams.add(languageModelParam);
-            prevWordsInfo = new PrevWordsInfo(languageModelParam.mTargetWord);
+            prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(
+                    new PrevWordsInfo.WordInfo(tempWord));
         }
         return languageModelParams;
     }
@@ -153,7 +154,7 @@
             final DistracterFilter distracterFilter) {
         final String word;
         if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST
-                && prevWordsInfo.mPrevWord == null && !isValidWord) {
+                && !prevWordsInfo.isValid() && !isValidWord) {
             word = targetWord.toLowerCase(locale);
         } else {
             word = targetWord;
@@ -167,7 +168,7 @@
         }
         final int unigramProbability = isValidWord ?
                 UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD;
-        if (prevWordsInfo.mPrevWord == null) {
+        if (!prevWordsInfo.isValid()) {
             if (DEBUG) {
                 Log.d(TAG, "--- add unigram: current("
                         + (isValidWord ? "Valid" : "OOV") + ") = " + word);
@@ -175,12 +176,12 @@
             return new LanguageModelParam(word, unigramProbability, timestamp);
         }
         if (DEBUG) {
-            Log.d(TAG, "--- add bigram: prev = " + prevWordsInfo.mPrevWord + ", current("
+            Log.d(TAG, "--- add bigram: prev = " + prevWordsInfo + ", current("
                     + (isValidWord ? "Valid" : "OOV") + ") = " + word);
         }
         final int bigramProbability = isValidWord ?
                 BIGRAM_PROBABILITY_FOR_VALID_WORD : BIGRAM_PROBABILITY_FOR_OOV_WORD;
-        return new LanguageModelParam(prevWordsInfo.mPrevWord, word, unigramProbability,
-                bigramProbability, timestamp);
+        return new LanguageModelParam(prevWordsInfo.mPrevWordsInfo[0].mWord, word,
+                unigramProbability, bigramProbability, timestamp);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java
new file mode 100644
index 0000000..4dfb38d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ScriptUtils.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import java.util.Locale;
+import java.util.TreeMap;
+
+/**
+ * A class to help with handling different writing scripts.
+ */
+public class ScriptUtils {
+    public static final int SCRIPT_LATIN = 0;
+    public static final int SCRIPT_CYRILLIC = 1;
+    public static final int SCRIPT_GREEK = 2;
+    public static final TreeMap<String, Integer> mLanguageToScript;
+    static {
+        // List of the supported languages and their associated script. We won't check
+        // words written in another script than the selected script, because we know we
+        // don't have those in our dictionary so we will underline everything and we
+        // will never have any suggestions, so it makes no sense checking them, and this
+        // is done in {@link #shouldFilterOut}. Also, the script is used to choose which
+        // proximity to pass to the dictionary descent algorithm.
+        // IMPORTANT: this only contains languages - do not write countries in there.
+        // Only the language is searched from the map.
+        mLanguageToScript = new TreeMap<>();
+        mLanguageToScript.put("cs", SCRIPT_LATIN);
+        mLanguageToScript.put("da", SCRIPT_LATIN);
+        mLanguageToScript.put("de", SCRIPT_LATIN);
+        mLanguageToScript.put("el", SCRIPT_GREEK);
+        mLanguageToScript.put("en", SCRIPT_LATIN);
+        mLanguageToScript.put("es", SCRIPT_LATIN);
+        mLanguageToScript.put("fi", SCRIPT_LATIN);
+        mLanguageToScript.put("fr", SCRIPT_LATIN);
+        mLanguageToScript.put("hr", SCRIPT_LATIN);
+        mLanguageToScript.put("it", SCRIPT_LATIN);
+        mLanguageToScript.put("lt", SCRIPT_LATIN);
+        mLanguageToScript.put("lv", SCRIPT_LATIN);
+        mLanguageToScript.put("nb", SCRIPT_LATIN);
+        mLanguageToScript.put("nl", SCRIPT_LATIN);
+        mLanguageToScript.put("pt", SCRIPT_LATIN);
+        mLanguageToScript.put("sl", SCRIPT_LATIN);
+        mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
+    }
+    /*
+     * Returns whether the code point is a letter that makes sense for the specified
+     * locale for this spell checker.
+     * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
+     * and is limited to EFIGS languages and Russian.
+     * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
+     * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
+     */
+    public static boolean isLetterCheckableByScript(final int codePoint, final int script) {
+        switch (script) {
+        case SCRIPT_LATIN:
+            // Our supported latin script dictionaries (EFIGS) at the moment only include
+            // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
+            // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
+            // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
+            // excluded from isLetter anyway.
+            return codePoint <= 0x2AF && Character.isLetter(codePoint);
+        case SCRIPT_CYRILLIC:
+            // All Cyrillic characters are in the 400~52F block. There are some in the upper
+            // Unicode range, but they are archaic characters that are not used in modern
+            // Russian and are not used by our dictionary.
+            return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
+        case SCRIPT_GREEK:
+            // Greek letters are either in the 370~3FF range (Greek & Coptic), or in the
+            // 1F00~1FFF range (Greek extended). Our dictionary contains both sort of characters.
+            // Our dictionary also contains a few words with 0xF2; it would be best to check
+            // if that's correct, but a web search does return results for these words so
+            // they are probably okay.
+            return (codePoint >= 0x370 && codePoint <= 0x3FF)
+                    || (codePoint >= 0x1F00 && codePoint <= 0x1FFF)
+                    || codePoint == 0xF2;
+        default:
+            // Should never come here
+            throw new RuntimeException("Impossible value of script: " + script);
+        }
+    }
+
+    public static int getScriptFromLocale(final Locale locale) {
+        final Integer script = mLanguageToScript.get(locale.getLanguage());
+        if (null == script) {
+            throw new RuntimeException("We have been called with an unsupported language: \""
+                    + locale.getLanguage() + "\". Framework bug?");
+        }
+        return script;
+    }
+}
diff --git a/native/jni/NativeFileList.mk b/native/jni/NativeFileList.mk
index 2dd75c4..fe21061 100644
--- a/native/jni/NativeFileList.mk
+++ b/native/jni/NativeFileList.mk
@@ -26,7 +26,6 @@
         dic_node_utils.cpp \
         dic_nodes_cache.cpp) \
     $(addprefix suggest/core/dictionary/, \
-        bigram_dictionary.cpp \
         dictionary.cpp \
         dictionary_utils.cpp \
         digraph_utils.cpp \
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 2654a4a..6b4fb79 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -283,7 +283,7 @@
     return dictionary->getMaxProbabilityOfExactMatches(codePoints, wordLength);
 }
 
-static jint latinime_BinaryDictionary_getBigramProbability(JNIEnv *env, jclass clazz,
+static jint latinime_BinaryDictionary_getNgramProbability(JNIEnv *env, jclass clazz,
         jlong dict, jintArray word0, jboolean isBeginningOfSentence, jintArray word1) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return JNI_FALSE;
@@ -294,7 +294,7 @@
     env->GetIntArrayRegion(word0, 0, word0Length, word0CodePoints);
     env->GetIntArrayRegion(word1, 0, word1Length, word1CodePoints);
     const PrevWordsInfo prevWordsInfo(word0CodePoints, word0Length, isBeginningOfSentence);
-    return dictionary->getBigramProbability(&prevWordsInfo, word1CodePoints, word1Length);
+    return dictionary->getNgramProbability(&prevWordsInfo, word1CodePoints, word1Length);
 }
 
 // Method to iterate all words in the dictionary for makedict.
@@ -355,7 +355,7 @@
             outShortcutProbabilities);
 }
 
-static bool latinime_BinaryDictionary_addUnigramWord(JNIEnv *env, jclass clazz, jlong dict,
+static bool latinime_BinaryDictionary_addUnigramEntry(JNIEnv *env, jclass clazz, jlong dict,
         jintArray word, jint probability, jintArray shortcutTarget, jint shortcutProbability,
         jboolean isBeginningOfSentence, jboolean isNotAWord, jboolean isBlacklisted,
         jint timestamp) {
@@ -378,7 +378,7 @@
     return dictionary->addUnigramEntry(codePoints, codePointCount, &unigramProperty);
 }
 
-static bool latinime_BinaryDictionary_removeUnigramWord(JNIEnv *env, jclass clazz, jlong dict,
+static bool latinime_BinaryDictionary_removeUnigramEntry(JNIEnv *env, jclass clazz, jlong dict,
         jintArray word) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) {
@@ -390,7 +390,7 @@
     return dictionary->removeUnigramEntry(codePoints, codePointCount);
 }
 
-static bool latinime_BinaryDictionary_addBigramWords(JNIEnv *env, jclass clazz, jlong dict,
+static bool latinime_BinaryDictionary_addNgramEntry(JNIEnv *env, jclass clazz, jlong dict,
         jintArray word0, jboolean isBeginningOfSentence, jintArray word1, jint probability,
         jint timestamp) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
@@ -412,7 +412,7 @@
     return dictionary->addNgramEntry(&prevWordsInfo, &bigramProperty);
 }
 
-static bool latinime_BinaryDictionary_removeBigramWords(JNIEnv *env, jclass clazz, jlong dict,
+static bool latinime_BinaryDictionary_removeNgramEntry(JNIEnv *env, jclass clazz, jlong dict,
         jintArray word0, jboolean isBeginningOfSentence, jintArray word1) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) {
@@ -686,9 +686,9 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_getMaxProbabilityOfExactMatches)
     },
     {
-        const_cast<char *>("getBigramProbabilityNative"),
+        const_cast<char *>("getNgramProbabilityNative"),
         const_cast<char *>("(J[IZ[I)I"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_getBigramProbability)
+        reinterpret_cast<void *>(latinime_BinaryDictionary_getNgramProbability)
     },
     {
         const_cast<char *>("getWordPropertyNative"),
@@ -702,24 +702,24 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_getNextWord)
     },
     {
-        const_cast<char *>("addUnigramWordNative"),
+        const_cast<char *>("addUnigramEntryNative"),
         const_cast<char *>("(J[II[IIZZZI)Z"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_addUnigramWord)
+        reinterpret_cast<void *>(latinime_BinaryDictionary_addUnigramEntry)
     },
     {
-        const_cast<char *>("removeUnigramWordNative"),
+        const_cast<char *>("removeUnigramEntryNative"),
         const_cast<char *>("(J[I)Z"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_removeUnigramWord)
+        reinterpret_cast<void *>(latinime_BinaryDictionary_removeUnigramEntry)
     },
     {
-        const_cast<char *>("addBigramWordsNative"),
+        const_cast<char *>("addNgramEntryNative"),
         const_cast<char *>("(J[IZ[III)Z"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_addBigramWords)
+        reinterpret_cast<void *>(latinime_BinaryDictionary_addNgramEntry)
     },
     {
-        const_cast<char *>("removeBigramWordsNative"),
+        const_cast<char *>("removeNgramEntryNative"),
         const_cast<char *>("(J[IZ[I)Z"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_removeBigramWords)
+        reinterpret_cast<void *>(latinime_BinaryDictionary_removeNgramEntry)
     },
     {
         const_cast<char *>("addMultipleDictionaryEntriesNative"),
diff --git a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp b/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
deleted file mode 100644
index 295e760..0000000
--- a/native/jni/src/suggest/core/dictionary/bigram_dictionary.cpp
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2010, 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.
- */
-
-#define LOG_TAG "LatinIME: bigram_dictionary.cpp"
-
-#include "bigram_dictionary.h"
-
-#include <algorithm>
-#include <cstring>
-
-#include "defines.h"
-#include "suggest/core/dictionary/binary_dictionary_bigrams_iterator.h"
-#include "suggest/core/dictionary/dictionary.h"
-#include "suggest/core/policy/dictionary_structure_with_buffer_policy.h"
-#include "suggest/core/result/suggestion_results.h"
-#include "suggest/core/session/prev_words_info.h"
-#include "utils/char_utils.h"
-
-namespace latinime {
-
-BigramDictionary::BigramDictionary(
-        const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy)
-        : mDictionaryStructurePolicy(dictionaryStructurePolicy) {
-    if (DEBUG_DICT) {
-        AKLOGI("BigramDictionary - constructor");
-    }
-}
-
-BigramDictionary::~BigramDictionary() {
-}
-
-/* Parameters :
- * prevWordsInfo: Information of previous words to get the predictions.
- * outSuggestionResults: SuggestionResults to put the predictions.
- */
-void BigramDictionary::getPredictions(const PrevWordsInfo *const prevWordsInfo,
-        SuggestionResults *const outSuggestionResults) const {
-    int unigramProbability = 0;
-    int bigramCodePoints[MAX_WORD_LENGTH];
-    BinaryDictionaryBigramsIterator bigramsIt =
-            prevWordsInfo->getBigramsIteratorForPrediction(mDictionaryStructurePolicy);
-    while (bigramsIt.hasNext()) {
-        bigramsIt.next();
-        if (bigramsIt.getBigramPos() == NOT_A_DICT_POS) {
-            continue;
-        }
-        const int codePointCount = mDictionaryStructurePolicy->
-                getCodePointsAndProbabilityAndReturnCodePointCount(bigramsIt.getBigramPos(),
-                        MAX_WORD_LENGTH, bigramCodePoints, &unigramProbability);
-        if (codePointCount <= 0) {
-            continue;
-        }
-        // Due to space constraints, the probability for bigrams is approximate - the lower the
-        // unigram probability, the worse the precision. The theoritical maximum error in
-        // resulting probability is 8 - although in the practice it's never bigger than 3 or 4
-        // in very bad cases. This means that sometimes, we'll see some bigrams interverted
-        // here, but it can't get too bad.
-        const int probability = mDictionaryStructurePolicy->getProbability(
-                unigramProbability, bigramsIt.getProbability());
-        outSuggestionResults->addPrediction(bigramCodePoints, codePointCount, probability);
-    }
-}
-
-// Returns a pointer to the start of the bigram list.
-// If the word is not found or has no bigrams, this function returns NOT_A_DICT_POS.
-int BigramDictionary::getBigramListPositionForWord(const int *prevWord, const int prevWordLength,
-        const bool forceLowerCaseSearch) const {
-    if (0 >= prevWordLength) return NOT_A_DICT_POS;
-    int pos = mDictionaryStructurePolicy->getTerminalPtNodePositionOfWord(prevWord, prevWordLength,
-            forceLowerCaseSearch);
-    if (NOT_A_DICT_POS == pos) return NOT_A_DICT_POS;
-    return mDictionaryStructurePolicy->getBigramsPositionOfPtNode(pos);
-}
-
-int BigramDictionary::getBigramProbability(const PrevWordsInfo *const prevWordsInfo,
-        const int *word1, int length1) const {
-    int nextWordPos = mDictionaryStructurePolicy->getTerminalPtNodePositionOfWord(word1, length1,
-            false /* forceLowerCaseSearch */);
-    if (NOT_A_DICT_POS == nextWordPos) return NOT_A_PROBABILITY;
-    BinaryDictionaryBigramsIterator bigramsIt =
-            prevWordsInfo->getBigramsIteratorForPrediction(mDictionaryStructurePolicy);
-    while (bigramsIt.hasNext()) {
-        bigramsIt.next();
-        if (bigramsIt.getBigramPos() == nextWordPos
-                && bigramsIt.getProbability() != NOT_A_PROBABILITY) {
-            return mDictionaryStructurePolicy->getProbability(
-                    mDictionaryStructurePolicy->getUnigramProbabilityOfPtNode(nextWordPos),
-                    bigramsIt.getProbability());
-        }
-    }
-    return NOT_A_PROBABILITY;
-}
-
-// TODO: Move functions related to bigram to here
-} // namespace latinime
diff --git a/native/jni/src/suggest/core/dictionary/bigram_dictionary.h b/native/jni/src/suggest/core/dictionary/bigram_dictionary.h
deleted file mode 100644
index bd3aed1..0000000
--- a/native/jni/src/suggest/core/dictionary/bigram_dictionary.h
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2010 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.
- */
-
-#ifndef LATINIME_BIGRAM_DICTIONARY_H
-#define LATINIME_BIGRAM_DICTIONARY_H
-
-#include "defines.h"
-
-namespace latinime {
-
-class DictionaryStructureWithBufferPolicy;
-class PrevWordsInfo;
-class SuggestionResults;
-
-class BigramDictionary {
- public:
-    BigramDictionary(const DictionaryStructureWithBufferPolicy *const dictionaryStructurePolicy);
-
-    void getPredictions(const PrevWordsInfo *const prevWordsInfo,
-            SuggestionResults *const outSuggestionResults) const;
-    int getBigramProbability(const PrevWordsInfo *const prevWordsInfo,
-            const int *word1, int length1) const;
-    ~BigramDictionary();
-
- private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(BigramDictionary);
-
-    int getBigramListPositionForWord(const int *prevWord, const int prevWordLength,
-            const bool forceLowerCaseSearch) const;
-
-    const DictionaryStructureWithBufferPolicy *const mDictionaryStructurePolicy;
-};
-} // namespace latinime
-#endif // LATINIME_BIGRAM_DICTIONARY_H
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 2282602..fb25f75 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -23,6 +23,7 @@
 #include "suggest/core/policy/dictionary_header_structure_policy.h"
 #include "suggest/core/result/suggestion_results.h"
 #include "suggest/core/session/dic_traverse_session.h"
+#include "suggest/core/session/prev_words_info.h"
 #include "suggest/core/suggest.h"
 #include "suggest/core/suggest_options.h"
 #include "suggest/policyimpl/gesture/gesture_suggest_policy_factory.h"
@@ -37,7 +38,6 @@
 Dictionary::Dictionary(JNIEnv *env, DictionaryStructureWithBufferPolicy::StructurePolicyPtr
         dictionaryStructureWithBufferPolicy)
         : mDictionaryStructureWithBufferPolicy(std::move(dictionaryStructureWithBufferPolicy)),
-          mBigramDictionary(mDictionaryStructureWithBufferPolicy.get()),
           mGestureSuggest(new Suggest(GestureSuggestPolicyFactory::getGestureSuggestPolicy())),
           mTypingSuggest(new Suggest(TypingSuggestPolicyFactory::getTypingSuggestPolicy())) {
     logDictionaryInfo(env);
@@ -62,7 +62,29 @@
 void Dictionary::getPredictions(const PrevWordsInfo *const prevWordsInfo,
         SuggestionResults *const outSuggestionResults) const {
     TimeKeeper::setCurrentTime();
-    mBigramDictionary.getPredictions(prevWordsInfo, outSuggestionResults);
+    int unigramProbability = 0;
+    int bigramCodePoints[MAX_WORD_LENGTH];
+    BinaryDictionaryBigramsIterator bigramsIt = prevWordsInfo->getBigramsIteratorForPrediction(
+            mDictionaryStructureWithBufferPolicy.get());
+    while (bigramsIt.hasNext()) {
+        bigramsIt.next();
+        if (bigramsIt.getBigramPos() == NOT_A_DICT_POS) {
+            continue;
+        }
+        if (prevWordsInfo->isNthPrevWordBeginningOfSentence(1 /* n */)
+                && bigramsIt.getProbability() == NOT_A_PROBABILITY) {
+            continue;
+        }
+        const int codePointCount = mDictionaryStructureWithBufferPolicy->
+                getCodePointsAndProbabilityAndReturnCodePointCount(bigramsIt.getBigramPos(),
+                        MAX_WORD_LENGTH, bigramCodePoints, &unigramProbability);
+        if (codePointCount <= 0) {
+            continue;
+        }
+        const int probability = mDictionaryStructureWithBufferPolicy->getProbability(
+                unigramProbability, bigramsIt.getProbability());
+        outSuggestionResults->addPrediction(bigramCodePoints, codePointCount, probability);
+    }
 }
 
 int Dictionary::getProbability(const int *word, int length) const {
@@ -81,10 +103,24 @@
             mDictionaryStructureWithBufferPolicy.get(), word, length);
 }
 
-int Dictionary::getBigramProbability(const PrevWordsInfo *const prevWordsInfo, const int *word,
+int Dictionary::getNgramProbability(const PrevWordsInfo *const prevWordsInfo, const int *word,
         int length) const {
     TimeKeeper::setCurrentTime();
-    return mBigramDictionary.getBigramProbability(prevWordsInfo, word, length);
+    int nextWordPos = mDictionaryStructureWithBufferPolicy->getTerminalPtNodePositionOfWord(word,
+            length, false /* forceLowerCaseSearch */);
+    if (NOT_A_DICT_POS == nextWordPos) return NOT_A_PROBABILITY;
+    BinaryDictionaryBigramsIterator bigramsIt = prevWordsInfo->getBigramsIteratorForPrediction(
+            mDictionaryStructureWithBufferPolicy.get());
+    while (bigramsIt.hasNext()) {
+        bigramsIt.next();
+        if (bigramsIt.getBigramPos() == nextWordPos
+                && bigramsIt.getProbability() != NOT_A_PROBABILITY) {
+            return mDictionaryStructureWithBufferPolicy->getProbability(
+                    mDictionaryStructureWithBufferPolicy->getUnigramProbabilityOfPtNode(
+                            nextWordPos), bigramsIt.getProbability());
+        }
+    }
+    return NOT_A_PROBABILITY;
 }
 
 bool Dictionary::addUnigramEntry(const int *const word, const int length,
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 247ee24..3b41088 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -21,7 +21,6 @@
 
 #include "defines.h"
 #include "jni.h"
-#include "suggest/core/dictionary/bigram_dictionary.h"
 #include "suggest/core/dictionary/property/word_property.h"
 #include "suggest/core/policy/dictionary_header_structure_policy.h"
 #include "suggest/core/policy/dictionary_structure_with_buffer_policy.h"
@@ -75,7 +74,7 @@
 
     int getMaxProbabilityOfExactMatches(const int *word, int length) const;
 
-    int getBigramProbability(const PrevWordsInfo *const prevWordsInfo,
+    int getNgramProbability(const PrevWordsInfo *const prevWordsInfo,
             const int *word, int length) const;
 
     bool addUnigramEntry(const int *const codePoints, const int codePointCount,
@@ -119,7 +118,6 @@
 
     const DictionaryStructureWithBufferPolicy::StructurePolicyPtr
             mDictionaryStructureWithBufferPolicy;
-    const BigramDictionary mBigramDictionary;
     const SuggestInterfacePtr mGestureSuggest;
     const SuggestInterfacePtr mTypingSuggest;
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
index 5c62b9c..002593c 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
@@ -268,6 +268,10 @@
 
 int PatriciaTriePolicy::getProbability(const int unigramProbability,
         const int bigramProbability) const {
+    // Due to space constraints, the probability for bigrams is approximate - the lower the unigram
+    // probability, the worse the precision. The theoritical maximum error in resulting probability
+    // is 8 - although in the practice it's never bigger than 3 or 4 in very bad cases. This means
+    // that sometimes, we'll see some bigrams interverted here, but it can't get too bad.
     if (unigramProbability == NOT_A_PROBABILITY) {
         return NOT_A_PROBABILITY;
     } else if (bigramProbability == NOT_A_PROBABILITY) {
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index 28cce83..ae18426 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -20,6 +20,7 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Pair;
 
+import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.CodePointUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
@@ -77,13 +78,13 @@
 
     private void addBigramWords(final BinaryDictionary binaryDictionary, final String word0,
             final String word1, final int probability) {
-        binaryDictionary.addNgramEntry(new PrevWordsInfo(word0), word1, probability,
+        binaryDictionary.addNgramEntry(new PrevWordsInfo(new WordInfo(word0)), word1, probability,
                 mCurrentTime /* timestamp */);
     }
 
     private static boolean isValidBigram(final BinaryDictionary binaryDictionary,
             final String word0, final String word1) {
-        return binaryDictionary.isValidNgram(new PrevWordsInfo(word0), word1);
+        return binaryDictionary.isValidNgram(new PrevWordsInfo(new WordInfo(word0)), word1);
     }
 
     private void forcePassingShortTime(final BinaryDictionary binaryDictionary) {
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index 83ea193..6ba18d6 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 import android.util.Pair;
 
+import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
 import com.android.inputmethod.latin.makedict.CodePointUtils;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.WeightedString;
@@ -203,23 +204,23 @@
 
     private static void addBigramWords(final BinaryDictionary binaryDictionary, final String word0,
             final String word1, final int probability) {
-        binaryDictionary.addNgramEntry(new PrevWordsInfo(word0), word1, probability,
+        binaryDictionary.addNgramEntry(new PrevWordsInfo(new WordInfo(word0)), word1, probability,
                 BinaryDictionary.NOT_A_VALID_TIMESTAMP /* timestamp */);
     }
 
     private static boolean isValidBigram(final BinaryDictionary binaryDictionary,
             final String word0, final String word1) {
-        return binaryDictionary.isValidNgram(new PrevWordsInfo(word0), word1);
+        return binaryDictionary.isValidNgram(new PrevWordsInfo(new WordInfo(word0)), word1);
     }
 
     private static void removeBigramEntry(final BinaryDictionary binaryDictionary,
             final String word0, final String word1) {
-        binaryDictionary.removeNgramEntry(new PrevWordsInfo(word0), word1);
+        binaryDictionary.removeNgramEntry(new PrevWordsInfo(new WordInfo(word0)), word1);
     }
 
     private static int getBigramProbability(final BinaryDictionary binaryDictionary,
             final String word0,  final String word1) {
-        return binaryDictionary.getNgramProbability(new PrevWordsInfo(word0), word1);
+        return binaryDictionary.getNgramProbability(new PrevWordsInfo(new WordInfo(word0)), word1);
     }
 
     public void testAddUnigramWord() {
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index 2709ecb..0552c22 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -517,15 +517,21 @@
                 suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
     }
 
-    public void testNoPredictionsAfterPeriod() {
+    public void testPredictionsAfterPeriod() {
         mLatinIME.clearPersonalizedDictionariesForTest();
         final String WORD_TO_TYPE = "Barack. ";
         type(WORD_TO_TYPE);
         sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
         runMessages();
-        // Test the first prediction is not displayed
-        final SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest();
-        assertEquals("no prediction after period", 0, suggestedWords.size());
+        SuggestedWords suggestedWords = mLatinIME.getSuggestedWordsForTest();
+        assertEquals("No prediction after period after inputting once.", 0, suggestedWords.size());
+
+        type(WORD_TO_TYPE);
+        sleep(DELAY_TO_WAIT_FOR_PREDICTIONS);
+        runMessages();
+        suggestedWords = mLatinIME.getSuggestedWordsForTest();
+        assertEquals("Beginning-of-Sentence prediction after inputting 2 times.", "Barack",
+                suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
     }
 
     public void testPredictionsAfterRecorrection() {
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
index 2c92bb3..5a5ec6d 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionAndTextRangeTests.java
@@ -156,16 +156,16 @@
     public void testGetPreviousWord() {
         // If one of the following cases breaks, the bigram suggestions won't work.
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def", mSpacingAndPunctuations, 2).mPrevWord, "abc");
+                "abc def", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
                 "abc", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE);
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
                 "abc. def", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE);
 
         assertFalse(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def", mSpacingAndPunctuations, 2).mIsBeginningOfSentence);
+                "abc def", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mIsBeginningOfSentence);
         assertTrue(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc", mSpacingAndPunctuations, 2).mIsBeginningOfSentence);
+                "abc", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mIsBeginningOfSentence);
         // The following tests reflect the current behavior of the function
         // RichInputConnection#getNthPreviousWord.
         // TODO: However at this time, the code does never go
@@ -174,20 +174,20 @@
         // logical. These tests are just there to catch any unintentional
         // changes in the behavior of the RichInputConnection#getPreviousWord method.
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def ", mSpacingAndPunctuations, 2).mPrevWord, "abc");
+                "abc def ", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def.", mSpacingAndPunctuations, 2).mPrevWord, "abc");
+                "abc def.", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "abc");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def .", mSpacingAndPunctuations, 2).mPrevWord, "def");
+                "abc def .", mSpacingAndPunctuations, 2).mPrevWordsInfo[0].mWord, "def");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
                 "abc ", mSpacingAndPunctuations, 2), PrevWordsInfo.BEGINNING_OF_SENTENCE);
 
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def", mSpacingAndPunctuations, 1).mPrevWord, "def");
+                "abc def", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "def");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc def ", mSpacingAndPunctuations, 1).mPrevWord, "def");
+                "abc def ", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "def");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
-                "abc 'def", mSpacingAndPunctuations, 1).mPrevWord, "'def");
+                "abc 'def", mSpacingAndPunctuations, 1).mPrevWordsInfo[0].mWord, "'def");
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
                 "abc def.", mSpacingAndPunctuations, 1), PrevWordsInfo.BEGINNING_OF_SENTENCE);
         assertEquals(RichInputConnection.getPrevWordsInfoFromNthPreviousWord(
diff --git a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index 8f32e53..76eaef4 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -104,7 +104,8 @@
         for (final WordProperty word0Property : dict) {
             if (null == word0Property.mBigrams) continue;
             for (final WeightedString word1 : word0Property.mBigrams) {
-                final PrevWordsInfo prevWordsInfo = new PrevWordsInfo(word0Property.mWord);
+                final PrevWordsInfo prevWordsInfo =
+                        new PrevWordsInfo(new PrevWordsInfo.WordInfo(word0Property.mWord));
                 if (!binaryDict.addNgramEntry(prevWordsInfo, word1.mWord,
                         word1.getProbability(), 0 /* timestamp */)) {
                     MakedictLog.e("Cannot add n-gram entry for "
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index 48d3a1c..f87f3b4 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 com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.DistracterFilter;
 import com.android.inputmethod.latin.utils.FileUtils;
@@ -115,7 +116,7 @@
             UserHistoryDictionary.addToDictionary(dict, prevWordsInfo, word, true,
                     (int)TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
                     DistracterFilter.EMPTY_DISTRACTER_FILTER);
-            prevWordsInfo = new PrevWordsInfo(word);
+            prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(new WordInfo(word));
         }
     }
 
@@ -262,11 +263,11 @@
         final UserHistoryDictionary dict =
                 PersonalizationHelper.getUserHistoryDictionary(getContext(), dummyLocale);
         dict.waitAllTasksForTests();
-        PrevWordsInfo prevWordsInfo = new PrevWordsInfo(null);
+        PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
         for (final String word : words) {
             UserHistoryDictionary.addToDictionary(dict, prevWordsInfo, word, true, mCurrentTime,
                     DistracterFilter.EMPTY_DISTRACTER_FILTER);
-            prevWordsInfo = new PrevWordsInfo(word);
+            prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo(new WordInfo(word));
             dict.waitAllTasksForTests();
             assertTrue(dict.isInDictionary(word));
         }