Merge "Add additional wait for writing file in testAddManyWords()."
diff --git a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java
new file mode 100644
index 0000000..2cec142
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.lang.reflect.Method;
+
+@UsedForTesting
+public final class CursorAnchorInfoCompatWrapper {
+    // Note that CursorAnchorInfo has been introduced in API level XX (Build.VERSION_CODE.LXX).
+    private static Class<?> getCursorAnchorInfoClass() {
+        try {
+            return Class.forName("android.view.inputmethod.CursorAnchorInfo");
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+    private static final Class<?> CLASS;
+    private static final Method METHOD_GET_CHARACTER_RECT;
+    private static final Method METHOD_GET_CHARACTER_RECT_FLAGS;
+    private static final Method METHOD_GET_COMPOSING_TEXT;
+    private static final Method METHOD_GET_COMPOSING_TEXT_START;
+    private static final Method METHOD_GET_MATRIX;
+    static {
+        CLASS = getCursorAnchorInfoClass();
+        METHOD_GET_CHARACTER_RECT = CompatUtils.getMethod(CLASS, "getCharacterRect", int.class);
+        METHOD_GET_CHARACTER_RECT_FLAGS = CompatUtils.getMethod(CLASS, "getCharacterRectFlags",
+                int.class);
+        METHOD_GET_COMPOSING_TEXT = CompatUtils.getMethod(CLASS, "getComposingText");
+        METHOD_GET_COMPOSING_TEXT_START = CompatUtils.getMethod(CLASS, "getComposingTextStart");
+        METHOD_GET_MATRIX = CompatUtils.getMethod(CLASS, "getMatrix");
+    }
+
+    @UsedForTesting
+    public static boolean isAvailable() {
+        return CLASS != null;
+    }
+
+    public static final int CHARACTER_RECT_TYPE_MASK = 0x0f;
+
+    /**
+     * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the editor did not specify any type of this
+     * character. Editor authors should not use this flag.
+     */
+    public static final int CHARACTER_RECT_TYPE_UNSPECIFIED = 0;
+
+    /**
+     * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the character is entirely visible.
+     */
+    public static final int CHARACTER_RECT_TYPE_FULLY_VISIBLE = 1;
+
+    /**
+     * Type for {@link #CHARACTER_RECT_TYPE_MASK}: some area of the character is invisible.
+     */
+    public static final int CHARACTER_RECT_TYPE_PARTIALLY_VISIBLE = 2;
+
+    /**
+     * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the character is entirely invisible.
+     */
+    public static final int CHARACTER_RECT_TYPE_INVISIBLE = 3;
+
+    /**
+     * Type for {@link #CHARACTER_RECT_TYPE_MASK}: the editor gave up to calculate the rectangle
+     * for this character. Input method authors should ignore the returned rectangle.
+     */
+    public static final int CHARACTER_RECT_TYPE_NOT_FEASIBLE = 4;
+
+    private Object mInstance;
+
+    private CursorAnchorInfoCompatWrapper(final Object instance) {
+        mInstance = instance;
+    }
+
+    @UsedForTesting
+    public static CursorAnchorInfoCompatWrapper fromObject(final Object instance) {
+        if (!isAvailable()) {
+            return new CursorAnchorInfoCompatWrapper(null);
+        }
+        return new CursorAnchorInfoCompatWrapper(instance);
+    }
+
+    private static final class FakeHolder {
+        static CursorAnchorInfoCompatWrapper sInstance = new CursorAnchorInfoCompatWrapper(null);
+    }
+
+    @UsedForTesting
+    public static CursorAnchorInfoCompatWrapper getFake() {
+        return FakeHolder.sInstance;
+    }
+
+    public CharSequence getComposingText() {
+        return (CharSequence) CompatUtils.invoke(mInstance, null, METHOD_GET_COMPOSING_TEXT);
+    }
+
+    private static int COMPOSING_TEXT_START_DEFAULT = -1;
+    public int getComposingTextStart() {
+        if (mInstance == null || METHOD_GET_COMPOSING_TEXT_START == null) {
+            return COMPOSING_TEXT_START_DEFAULT;
+        }
+        return (int) CompatUtils.invoke(mInstance, null, METHOD_GET_COMPOSING_TEXT_START);
+    }
+
+    public Matrix getMatrix() {
+        return (Matrix) CompatUtils.invoke(mInstance, null, METHOD_GET_MATRIX);
+    }
+
+    public RectF getCharacterRect(final int index) {
+        return (RectF) CompatUtils.invoke(mInstance, null, METHOD_GET_CHARACTER_RECT, index);
+    }
+
+    public int getCharacterRectFlags(final int index) {
+        if (mInstance == null || METHOD_GET_CHARACTER_RECT_FLAGS == null) {
+            return CHARACTER_RECT_TYPE_UNSPECIFIED;
+        }
+        return (int) CompatUtils.invoke(mInstance, null, METHOD_GET_CHARACTER_RECT_FLAGS, index);
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index f351267..140e768 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.preference.PreferenceManager;
 import android.util.Log;
@@ -233,11 +234,21 @@
     }
 
     private void setMainKeyboardFrame() {
-        mMainKeyboardFrame.setVisibility(View.VISIBLE);
+        mMainKeyboardFrame.setVisibility(hasHardwareKeyboard() ? View.GONE : View.VISIBLE);
         mEmojiPalettesView.setVisibility(View.GONE);
         mEmojiPalettesView.stopEmojiPalettes();
     }
 
+    // TODO: Move this boolean to a member of {@link SettingsValues} and reset it
+    // at {@link LatinIME#onConfigurationChanged(Configuration)}.
+    public boolean hasHardwareKeyboard() {
+        // Copied from {@link InputMethodServce#onEvaluateInputViewShown()}.
+        final Configuration config = mLatinIME.getResources().getConfiguration();
+        final boolean noHardwareKeyboard = config.keyboard == Configuration.KEYBOARD_NOKEYS
+                || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES;
+        return !noHardwareKeyboard;
+    }
+
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setEmojiKeyboard() {
@@ -249,6 +260,14 @@
         mEmojiPalettesView.setVisibility(View.VISIBLE);
     }
 
+    public void onToggleEmojiKeyboard() {
+        if (isShowingEmojiPalettes()) {
+            setAlphabetKeyboard();
+        } else {
+            setEmojiKeyboard();
+        }
+    }
+
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setSymbolsShiftedKeyboard() {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java
index 4c2e0dd..1354233 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java
@@ -38,6 +38,9 @@
     public static final int THEME_ID_LXX_DARK = 4;
     public static final int DEFAULT_THEME_ID = THEME_ID_KLP;
 
+    // TODO: Update this constant once the *next* version becomes available.
+    public static final int VERSION_CODES_LXX = 21;
+
     private static final KeyboardTheme[] KEYBOARD_THEMES = {
         new KeyboardTheme(THEME_ID_ICS, R.style.KeyboardTheme_ICS,
                 // This has never been selected because we support ICS or later.
@@ -47,8 +50,7 @@
                 VERSION_CODES.ICE_CREAM_SANDWICH),
         new KeyboardTheme(THEME_ID_LXX_LIGHT, R.style.KeyboardTheme_LXX_Light,
                 // Default theme for LXX.
-                // TODO: Update this constant once the *next* version becomes available.
-                VERSION_CODES.CUR_DEVELOPMENT),
+                VERSION_CODES_LXX),
         new KeyboardTheme(THEME_ID_LXX_DARK, R.style.KeyboardTheme_LXX_Dark,
                 VERSION_CODES.BASE),
     };
@@ -100,12 +102,7 @@
     }
 
     private static int getSdkVersion() {
-        final int sdkVersion = Build.VERSION.SDK_INT;
-        // TODO: Consider to remove this check once the *next* version becomes available.
-        if (sdkVersion > VERSION_CODES.KITKAT) {
-            return VERSION_CODES.CUR_DEVELOPMENT;
-        }
-        return sdkVersion;
+        return Build.VERSION.SDK_INT;
     }
 
     @UsedForTesting
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 702efb3..1ef53a6 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -46,6 +46,7 @@
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
 import com.android.inputmethod.keyboard.internal.KeyPreviewChoreographer;
 import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
+import com.android.inputmethod.keyboard.internal.KeyPreviewView;
 import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper;
 import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.keyboard.internal.NonDistinctMultitouchHelper;
@@ -764,6 +765,9 @@
     public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
             final int languageOnSpacebarFormatType,
             final boolean hasMultipleEnabledIMEsOrSubtypes) {
+        if (subtypeChanged) {
+            KeyPreviewView.clearTextCache();
+        }
         mLanguageOnSpacebarFormatType = languageOnSpacebarFormatType;
         mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
         final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java
index 360faf8..2453860 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewView.java
@@ -17,7 +17,10 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.text.TextPaint;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.TypedValue;
 import android.view.Gravity;
@@ -26,6 +29,8 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.latin.R;
 
+import java.util.HashSet;
+
 /**
  * The pop up key preview view.
  */
@@ -34,6 +39,9 @@
     public static final int POSITION_LEFT = 1;
     public static final int POSITION_RIGHT = 2;
 
+    private final Rect mBackgroundPadding = new Rect();
+    private static final HashSet<String> sNoScaleXTextSet = new HashSet<>();
+
     public KeyPreviewView(final Context context, final AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -58,7 +66,48 @@
         setTextSize(TypedValue.COMPLEX_UNIT_PX, key.selectPreviewTextSize(drawParams));
         setTypeface(key.selectPreviewTypeface(drawParams));
         // TODO Should take care of temporaryShiftLabel here.
-        setText(key.getPreviewLabel());
+        setTextAndScaleX(key.getPreviewLabel());
+    }
+
+    private void setTextAndScaleX(final String text) {
+        setTextScaleX(1.0f);
+        setText(text);
+        if (sNoScaleXTextSet.contains(text)) {
+            return;
+        }
+        // TODO: Override {@link #setBackground(Drawable)} that is supported from API 16 and
+        // calculate maximum text width.
+        final Drawable background = getBackground();
+        if (background == null) {
+            return;
+        }
+        background.getPadding(mBackgroundPadding);
+        final int maxWidth = background.getIntrinsicWidth() - mBackgroundPadding.left
+                - mBackgroundPadding.right;
+        final float width = getTextWidth(text, getPaint());
+        if (width <= maxWidth) {
+            sNoScaleXTextSet.add(text);
+            return;
+        }
+        setTextScaleX(maxWidth / width);
+    }
+
+    public static void clearTextCache() {
+        sNoScaleXTextSet.clear();
+    }
+
+    private static float getTextWidth(final String text, final TextPaint paint) {
+        if (TextUtils.isEmpty(text)) {
+            return 0.0f;
+        }
+        final int len = text.length();
+        final float[] widths = new float[len];
+        final int count = paint.getTextWidths(text, 0, len, widths);
+        float width = 0;
+        for (int i = 0; i < count; i++) {
+            width += widths[i];
+        }
+        return width;
     }
 
     // Background state set
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 2e3cd6b..9b629ca 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -145,6 +145,8 @@
     // If it turns out we need several, it will get grown seamlessly.
     final SparseArray<HardwareEventDecoder> mHardwareEventDecoders = new SparseArray<>(1);
 
+    // TODO: Move these {@link View}s to {@link KeyboardSwitcher}.
+    private View mInputView;
     private View mExtractArea;
     private View mKeyPreviewBackingView;
     private SuggestionStripView mSuggestionStripView;
@@ -153,6 +155,7 @@
     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
     private final SubtypeSwitcher mSubtypeSwitcher;
     private final SubtypeState mSubtypeState = new SubtypeState();
+    private final SpecialKeyDetector mSpecialKeyDetector = new SpecialKeyDetector();
 
     // Object for reacting to adding/removing a dictionary pack.
     private final BroadcastReceiver mDictionaryPackInstallReceiver =
@@ -709,6 +712,7 @@
     @Override
     public void setInputView(final View view) {
         super.setInputView(view);
+        mInputView = view;
         mExtractArea = getWindow().getWindow().getDecorView()
                 .findViewById(android.R.id.extractArea);
         mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
@@ -1079,6 +1083,14 @@
         if (visibleKeyboardView == null || !hasSuggestionStripView()) {
             return;
         }
+        final boolean hasHardwareKeyboard = mKeyboardSwitcher.hasHardwareKeyboard();
+        if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) {
+            // If there is a hardware keyboard and a visible software keyboard view has been hidden,
+            // no visual element will be shown on the screen.
+            outInsets.touchableInsets = mInputView.getHeight();
+            outInsets.visibleTopInsets = mInputView.getHeight();
+            return;
+        }
         final int adjustedBackingHeight = getAdjustedBackingViewHeight();
         final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
         final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
@@ -1111,7 +1123,17 @@
     }
 
     @Override
+    public boolean onEvaluateInputViewShown() {
+        // Always show {@link InputView}.
+        return true;
+    }
+
+    @Override
     public boolean onEvaluateFullscreenMode() {
+        if (mKeyboardSwitcher.hasHardwareKeyboard()) {
+            // If there is a hardware keyboard, disable full screen mode.
+            return false;
+        }
         // Reread resource value here, because this method is called by the framework as needed.
         final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources());
         if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
@@ -1568,6 +1590,7 @@
     // Hooks for hardware keyboard
     @Override
     public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
+        mSpecialKeyDetector.onKeyDown(keyEvent);
         if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
             return super.onKeyDown(keyCode, keyEvent);
         }
@@ -1587,12 +1610,16 @@
     }
 
     @Override
-    public boolean onKeyUp(final int keyCode, final KeyEvent event) {
-        final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
+    public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
+        mSpecialKeyDetector.onKeyUp(keyEvent);
+        if (!ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) {
+            return super.onKeyUp(keyCode, keyEvent);
+        }
+        final long keyIdentifier = keyEvent.getDeviceId() << 32 + keyEvent.getKeyCode();
         if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
             return true;
         }
-        return super.onKeyUp(keyCode, event);
+        return super.onKeyUp(keyCode, keyEvent);
     }
 
     // onKeyDown and onKeyUp are the main events we are interested in. There are two more events
diff --git a/java/src/com/android/inputmethod/latin/SpecialKeyDetector.java b/java/src/com/android/inputmethod/latin/SpecialKeyDetector.java
new file mode 100644
index 0000000..9d6c69a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/SpecialKeyDetector.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.view.KeyEvent;
+
+final class SpecialKeyDetector {
+    /**
+     * Record a down key event.
+     * @param keyEvent a down key event.
+     */
+    public void onKeyDown(final KeyEvent keyEvent) {
+    }
+
+    /**
+     * Record an up key event.
+     * @param keyEvent an up key event.
+     */
+    public void onKeyUp(final KeyEvent keyEvent) {
+    }
+}
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.cpp
index 278f2b1..f7179f6 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.cpp
@@ -234,8 +234,8 @@
 bool Ver4PatriciaTrieNodeWriter::addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
         const BigramProperty *const bigramProperty, bool *const outAddedNewEntry) {
     if (!mBigramPolicy->addNewEntry(prevWordIds[0], wordId, bigramProperty, outAddedNewEntry)) {
-        AKLOGE("Cannot add new bigram entry. terminalId: %d, targetTerminalId: %d",
-                sourcePtNodeParams->getTerminalId(), targetPtNodeParam->getTerminalId());
+        AKLOGE("Cannot add new bigram entry. prevWordId: %d, wordId: %d",
+                prevWordIds[0], wordId);
         return false;
     }
     const int ptNodePos =
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
index 5dc91ba..f3bc4a0 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
@@ -46,7 +46,7 @@
 
 bool LanguageModelDictContent::setNgramProbabilityEntry(const WordIdArrayView prevWordIds,
         const int terminalId, const ProbabilityEntry *const probabilityEntry) {
-    const int bitmapEntryIndex = getBitmapEntryIndex(prevWordIds);
+    const int bitmapEntryIndex = createAndGetBitmapEntryIndex(prevWordIds);
     if (bitmapEntryIndex == TrieMap::INVALID_INDEX) {
         return false;
     }
@@ -80,6 +80,19 @@
     return true;
 }
 
+int LanguageModelDictContent::createAndGetBitmapEntryIndex(const WordIdArrayView prevWordIds) {
+    if (prevWordIds.empty()) {
+        return mTrieMap.getRootBitmapEntryIndex();
+    }
+    const int lastBitmapEntryIndex =
+            getBitmapEntryIndex(prevWordIds.limit(prevWordIds.size() - 1));
+    if (lastBitmapEntryIndex == TrieMap::INVALID_INDEX) {
+        return TrieMap::INVALID_INDEX;
+    }
+    return mTrieMap.getNextLevelBitmapEntryIndex(prevWordIds[prevWordIds.size() - 1],
+            lastBitmapEntryIndex);
+}
+
 int LanguageModelDictContent::getBitmapEntryIndex(const WordIdArrayView prevWordIds) const {
     int bitmapEntryIndex = mTrieMap.getRootBitmapEntryIndex();
     for (const int wordId : prevWordIds) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
index 18f2e01..104ee25 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
@@ -76,7 +76,7 @@
     bool runGCInner(const TerminalPositionLookupTable::TerminalIdMap *const terminalIdMap,
             const TrieMap::TrieMapRange trieMapRange, const int nextLevelBitmapEntryIndex,
             int *const outNgramCount);
-
+    int createAndGetBitmapEntryIndex(const WordIdArrayView prevWordIds);
     int getBitmapEntryIndex(const WordIdArrayView prevWordIds) const;
 };
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
index feff6b5..ed77bd2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
@@ -21,6 +21,8 @@
 #include <cstdint>
 
 #include "defines.h"
+#include "suggest/core/dictionary/property/bigram_property.h"
+#include "suggest/core/dictionary/property/unigram_property.h"
 #include "suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h"
 #include "suggest/policyimpl/dictionary/utils/historical_info.h"
 
@@ -45,6 +47,20 @@
             const HistoricalInfo *const historicalInfo)
             : mFlags(flags), mProbability(probability), mHistoricalInfo(*historicalInfo) {}
 
+    // Create from unigram property.
+    // TODO: Set flags.
+    ProbabilityEntry(const UnigramProperty *const unigramProperty)
+            : mFlags(0), mProbability(unigramProperty->getProbability()),
+              mHistoricalInfo(unigramProperty->getTimestamp(), unigramProperty->getLevel(),
+                      unigramProperty->getCount()) {}
+
+    // Create from bigram property.
+    // TODO: Set flags.
+    ProbabilityEntry(const BigramProperty *const bigramProperty)
+            : mFlags(0), mProbability(bigramProperty->getProbability()),
+              mHistoricalInfo(bigramProperty->getTimestamp(), bigramProperty->getLevel(),
+                      bigramProperty->getCount()) {}
+
     const ProbabilityEntry createEntryWithUpdatedProbability(const int probability) const {
         return ProbabilityEntry(mFlags, probability, &mHistoricalInfo);
     }
@@ -54,6 +70,10 @@
         return ProbabilityEntry(mFlags, mProbability, historicalInfo);
     }
 
+    bool isValid() const {
+        return (mProbability != NOT_A_PROBABILITY) || hasHistoricalInfo();
+    }
+
     bool hasHistoricalInfo() const {
         return mHistoricalInfo.isValid();
     }
@@ -89,7 +109,7 @@
     static ProbabilityEntry decode(const uint64_t encodedEntry, const bool hasHistoricalInfo) {
         if (hasHistoricalInfo) {
             const int flags = readFromEncodedEntry(encodedEntry,
-                    Ver4DictConstants::FLAGS_IN_PROBABILITY_FILE_SIZE,
+                    Ver4DictConstants::FLAGS_IN_LANGUAGE_MODEL_SIZE,
                     Ver4DictConstants::TIME_STAMP_FIELD_SIZE
                             + Ver4DictConstants::WORD_LEVEL_FIELD_SIZE
                             + Ver4DictConstants::WORD_COUNT_FIELD_SIZE);
@@ -106,7 +126,7 @@
             return ProbabilityEntry(flags, NOT_A_PROBABILITY, &historicalInfo);
         } else {
             const int flags = readFromEncodedEntry(encodedEntry,
-                    Ver4DictConstants::FLAGS_IN_PROBABILITY_FILE_SIZE,
+                    Ver4DictConstants::FLAGS_IN_LANGUAGE_MODEL_SIZE,
                     Ver4DictConstants::PROBABILITY_SIZE);
             const int probability = readFromEncodedEntry(encodedEntry,
                     Ver4DictConstants::PROBABILITY_SIZE, 0 /* pos */);
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 93d4e56..e622442 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
@@ -46,7 +46,7 @@
 
 const int Ver4DictConstants::NOT_A_TERMINAL_ID = -1;
 const int Ver4DictConstants::PROBABILITY_SIZE = 1;
-const int Ver4DictConstants::FLAGS_IN_PROBABILITY_FILE_SIZE = 1;
+const int Ver4DictConstants::FLAGS_IN_LANGUAGE_MODEL_SIZE = 1;
 const int Ver4DictConstants::TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
 const int Ver4DictConstants::NOT_A_TERMINAL_ADDRESS = 0;
 const int Ver4DictConstants::TERMINAL_ID_FIELD_SIZE = 4;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
index 6950ca7..8d29f60 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
@@ -41,7 +41,7 @@
 
     static const int NOT_A_TERMINAL_ID;
     static const int PROBABILITY_SIZE;
-    static const int FLAGS_IN_PROBABILITY_FILE_SIZE;
+    static const int FLAGS_IN_LANGUAGE_MODEL_SIZE;
     static const int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE;
     static const int NOT_A_TERMINAL_ADDRESS;
     static const int TERMINAL_ID_FIELD_SIZE;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
index 857222f..2c848cb 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
@@ -145,10 +145,11 @@
     const ProbabilityEntry originalProbabilityEntry =
             mBuffers->getLanguageModelDictContent()->getProbabilityEntry(
                     toBeUpdatedPtNodeParams->getTerminalId());
-    const ProbabilityEntry probabilityEntry = createUpdatedEntryFrom(&originalProbabilityEntry,
-            unigramProperty);
+    const ProbabilityEntry probabilityEntryOfUnigramProperty = ProbabilityEntry(unigramProperty);
+    const ProbabilityEntry updatedProbabilityEntry =
+            createUpdatedEntryFrom(&originalProbabilityEntry, &probabilityEntryOfUnigramProperty);
     return mBuffers->getMutableLanguageModelDictContent()->setProbabilityEntry(
-            toBeUpdatedPtNodeParams->getTerminalId(), &probabilityEntry);
+            toBeUpdatedPtNodeParams->getTerminalId(), &updatedProbabilityEntry);
 }
 
 bool Ver4PatriciaTrieNodeWriter::updatePtNodeProbabilityAndGetNeedsToKeepPtNodeAfterGC(
@@ -216,16 +217,36 @@
     }
     // Write probability.
     ProbabilityEntry newProbabilityEntry;
+    const ProbabilityEntry probabilityEntryOfUnigramProperty = ProbabilityEntry(unigramProperty);
     const ProbabilityEntry probabilityEntryToWrite = createUpdatedEntryFrom(
-            &newProbabilityEntry, unigramProperty);
+            &newProbabilityEntry, &probabilityEntryOfUnigramProperty);
     return mBuffers->getMutableLanguageModelDictContent()->setProbabilityEntry(
             terminalId, &probabilityEntryToWrite);
 }
 
 bool Ver4PatriciaTrieNodeWriter::addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
         const BigramProperty *const bigramProperty, bool *const outAddedNewBigram) {
+    // TODO: Support n-gram.
+    LanguageModelDictContent *const languageModelDictContent =
+            mBuffers->getMutableLanguageModelDictContent();
+    const ProbabilityEntry probabilityEntry =
+            languageModelDictContent->getNgramProbabilityEntry(
+                    prevWordIds.limit(1 /* maxSize */), wordId);
+    const ProbabilityEntry probabilityEntryOfBigramProperty(bigramProperty);
+    const ProbabilityEntry updatedProbabilityEntry = createUpdatedEntryFrom(
+            &probabilityEntry, &probabilityEntryOfBigramProperty);
+    if (!languageModelDictContent->setNgramProbabilityEntry(
+            prevWordIds.limit(1 /* maxSize */), wordId, &updatedProbabilityEntry)) {
+        AKLOGE("Cannot add new ngram entry. prevWordId: %d, wordId: %d",
+                prevWordIds[0], wordId);
+        return false;
+    }
+    if (!probabilityEntry.isValid() && outAddedNewBigram) {
+        *outAddedNewBigram = true;
+    }
+    // TODO: Remove.
     if (!mBigramPolicy->addNewEntry(prevWordIds[0], wordId, bigramProperty, outAddedNewBigram)) {
-        AKLOGE("Cannot add new bigram entry. terminalId: %d, targetTerminalId: %d",
+        AKLOGE("Cannot add new bigram entry. prevWordId: %d, wordId: %d",
                 prevWordIds[0], wordId);
         return false;
     }
@@ -234,6 +255,7 @@
 
 bool Ver4PatriciaTrieNodeWriter::removeNgramEntry(const WordIdArrayView prevWordIds,
         const int wordId) {
+    // TODO: Remove.
     return mBigramPolicy->removeEntry(prevWordIds[0], wordId);
 }
 
@@ -352,20 +374,19 @@
 
 const ProbabilityEntry Ver4PatriciaTrieNodeWriter::createUpdatedEntryFrom(
         const ProbabilityEntry *const originalProbabilityEntry,
-        const UnigramProperty *const unigramProperty) const {
+        const ProbabilityEntry *const probabilityEntry) const {
     // TODO: Consolidate historical info and probability.
     if (mHeaderPolicy->hasHistoricalInfoOfWords()) {
-        const HistoricalInfo historicalInfoForUpdate(unigramProperty->getTimestamp(),
-                unigramProperty->getLevel(), unigramProperty->getCount());
         const HistoricalInfo updatedHistoricalInfo =
                 ForgettingCurveUtils::createUpdatedHistoricalInfo(
                         originalProbabilityEntry->getHistoricalInfo(),
-                        unigramProperty->getProbability(), &historicalInfoForUpdate, mHeaderPolicy);
+                        probabilityEntry->getProbability(), probabilityEntry->getHistoricalInfo(),
+                        mHeaderPolicy);
         return originalProbabilityEntry->createEntryWithUpdatedHistoricalInfo(
                 &updatedHistoricalInfo);
     } else {
         return originalProbabilityEntry->createEntryWithUpdatedProbability(
-                unigramProperty->getProbability());
+                probabilityEntry->getProbability());
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
index 6703dba..5d73b6e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.h
@@ -98,12 +98,12 @@
             const PtNodeParams *const ptNodeParams, int *const outTerminalId,
             int *const ptNodeWritingPos);
 
-    // Create updated probability entry using given unigram property. In addition to the
+    // Create updated probability entry using given probability property. In addition to the
     // probability, this method updates historical information if needed.
-    // TODO: Update flags belonging to the unigram property.
+    // TODO: Update flags.
     const ProbabilityEntry createUpdatedEntryFrom(
             const ProbabilityEntry *const originalProbabilityEntry,
-            const UnigramProperty *const unigramProperty) const;
+            const ProbabilityEntry *const probabilityEntry) const;
 
     bool updatePtNodeFlags(const int ptNodePos, const bool isBlacklisted, const bool isNotAWord,
             const bool isTerminal, const bool hasMultipleChars);
diff --git a/native/jni/src/utils/int_array_view.h b/native/jni/src/utils/int_array_view.h
index c1ddc98..53f2d29 100644
--- a/native/jni/src/utils/int_array_view.h
+++ b/native/jni/src/utils/int_array_view.h
@@ -91,6 +91,11 @@
         return mPtr + mSize;
     }
 
+    // Returns the view whose size is smaller than or equal to the given count.
+    const IntArrayView limit(const size_t maxSize) const {
+        return IntArrayView(mPtr, std::min(maxSize, mSize));
+    }
+
  private:
     DISALLOW_ASSIGNMENT_OPERATOR(IntArrayView);
 
diff --git a/native/jni/tests/utils/int_array_view_test.cpp b/native/jni/tests/utils/int_array_view_test.cpp
index bd843ab..ecc451a 100644
--- a/native/jni/tests/utils/int_array_view_test.cpp
+++ b/native/jni/tests/utils/int_array_view_test.cpp
@@ -53,9 +53,24 @@
 TEST(IntArrayViewTest, TestConstructFromObject) {
     const int object = 10;
     const auto intArrayView = IntArrayView::fromObject(&object);
-    EXPECT_EQ(1, intArrayView.size());
+    EXPECT_EQ(1u, intArrayView.size());
     EXPECT_EQ(object, intArrayView[0]);
 }
 
+TEST(IntArrayViewTest, TestLimit) {
+    const std::vector<int> intVector = {3, 2, 1, 0, -1, -2};
+    IntArrayView intArrayView(intVector);
+
+    EXPECT_TRUE(intArrayView.limit(0).empty());
+    EXPECT_EQ(intArrayView.size(), intArrayView.limit(intArrayView.size()).size());
+    EXPECT_EQ(intArrayView.size(), intArrayView.limit(1000).size());
+
+    IntArrayView subView = intArrayView.limit(4);
+    EXPECT_EQ(4u, subView.size());
+    for (size_t i = 0; i < subView.size(); ++i) {
+        EXPECT_EQ(intVector[i], subView[i]);
+    }
+}
+
 }  // namespace
 }  // namespace latinime
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java b/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
index 0c7e400..45e1558 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardThemeTests.java
@@ -20,6 +20,7 @@
 import static com.android.inputmethod.keyboard.KeyboardTheme.THEME_ID_KLP;
 import static com.android.inputmethod.keyboard.KeyboardTheme.THEME_ID_LXX_DARK;
 import static com.android.inputmethod.keyboard.KeyboardTheme.THEME_ID_LXX_LIGHT;
+import static com.android.inputmethod.keyboard.KeyboardTheme.VERSION_CODES_LXX;
 
 import android.content.SharedPreferences;
 import android.os.Build.VERSION_CODES;
@@ -31,9 +32,6 @@
 public class KeyboardThemeTests extends AndroidTestCase {
     private SharedPreferences mPrefs;
 
-    // TODO: Remove this constant once the *next* version becomes available.
-    private static final int VERSION_CODES_LXX = VERSION_CODES.CUR_DEVELOPMENT;
-
     private static final int THEME_ID_NULL = -1;
     private static final int THEME_ID_UNKNOWN = -2;
     private static final int THEME_ID_ILLEGAL = -3;
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsMalayalamIN.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsMalayalamIN.java
index f937de8..b494ad3 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsMalayalamIN.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsMalayalamIN.java
@@ -25,7 +25,7 @@
 import java.util.Locale;
 
 /**
- * ta_IN: Malayalam (India)/malayalam
+ * ml_IN: Malayalam (India)/malayalam
  */
 @SmallTest
 public final class TestsMalayalamIN extends LayoutTestsBase {