Merge "Make dict log cleanup." into lmp-dev
diff --git a/java/res/drawable-hdpi/ic_add_circle_wht_24dp.png b/java/res/drawable-hdpi/ic_add_circle_wht_24dp.png
new file mode 100644
index 0000000..314c521
--- /dev/null
+++ b/java/res/drawable-hdpi/ic_add_circle_wht_24dp.png
Binary files differ
diff --git a/java/res/drawable-mdpi/ic_add_circle_wht_24dp.png b/java/res/drawable-mdpi/ic_add_circle_wht_24dp.png
new file mode 100644
index 0000000..11363b1
--- /dev/null
+++ b/java/res/drawable-mdpi/ic_add_circle_wht_24dp.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/ic_add_circle_wht_24dp.png b/java/res/drawable-xhdpi/ic_add_circle_wht_24dp.png
new file mode 100644
index 0000000..32a5b05
--- /dev/null
+++ b/java/res/drawable-xhdpi/ic_add_circle_wht_24dp.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/ic_add_circle_wht_24dp.png b/java/res/drawable-xxhdpi/ic_add_circle_wht_24dp.png
new file mode 100644
index 0000000..a22c463
--- /dev/null
+++ b/java/res/drawable-xxhdpi/ic_add_circle_wht_24dp.png
Binary files differ
diff --git a/java/res/menu/add_style.xml b/java/res/menu/add_style.xml
new file mode 100644
index 0000000..d1cab4b
--- /dev/null
+++ b/java/res/menu/add_style.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/action_add_style"
+        android:icon="@drawable/ic_add_circle_wht_24dp"
+        android:title="@string/add_style"
+        android:showAsAction="always" />
+</menu>
\ No newline at end of file
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 003b011..9a22273 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -52,7 +52,9 @@
         <attr name="spacebarIconWidthRatio" format="float" />
         <!-- Right padding of hint letter to the edge of the key.-->
         <attr name="keyHintLetterPadding" format="dimension" />
-        <!-- Bottom padding of popup hint letter "..." to the edge of the key.-->
+        <!-- Popup hint letter string-->
+        <attr name="keyPopupHintLetter" format="string" />
+        <!-- Bottom padding of popup hint letter to the edge of the key.-->
         <attr name="keyPopupHintLetterPadding" format="dimension" />
         <!-- Right padding of shifted letter hint to the edge of the key.-->
         <attr name="keyShiftedLetterHintPadding" format="dimension" />
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index 6118ce1..a6f390c 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -60,6 +60,8 @@
         <item name="keyPreviewTextColor">@color/key_text_color_holo</item>
         <!-- A negative value to disable key text shadow layer. -->
         <item name="keyTextShadowRadius">-1.0</item>
+        <!-- U+2026: "…" HORIZONTAL ELLIPSIS -->
+        <item name="keyPopupHintLetter">&#x2026;</item>
     </style>
     <style
         name="MainKeyboardView.ICS"
diff --git a/java/res/values/themes-klp.xml b/java/res/values/themes-klp.xml
index 1933860..8782a76 100644
--- a/java/res/values/themes-klp.xml
+++ b/java/res/values/themes-klp.xml
@@ -60,6 +60,8 @@
         <item name="keyPreviewTextColor">@color/key_text_color_holo</item>
         <!-- A negative value to disable key text shadow layer. -->
         <item name="keyTextShadowRadius">-1.0</item>
+        <!-- U+2026: "…" HORIZONTAL ELLIPSIS -->
+        <item name="keyPopupHintLetter">&#x2026;</item>
     </style>
     <style
         name="MainKeyboardView.KLP"
diff --git a/java/res/values/themes-lxx-dark.xml b/java/res/values/themes-lxx-dark.xml
index 0a13158..fa6aa62 100644
--- a/java/res/values/themes-lxx-dark.xml
+++ b/java/res/values/themes-lxx-dark.xml
@@ -61,6 +61,7 @@
         <item name="keyPreviewTextColor">@color/key_text_color_lxx_dark</item>
         <!-- A negative value to disable key text shadow layer. -->
         <item name="keyTextShadowRadius">-1.0</item>
+        <item name="keyPopupHintLetter"></item> <!-- No popup hint letter -->
     </style>
     <style
         name="MainKeyboardView.LXX_Dark"
diff --git a/java/res/values/themes-lxx-light.xml b/java/res/values/themes-lxx-light.xml
index eccecdd..e7350f9 100644
--- a/java/res/values/themes-lxx-light.xml
+++ b/java/res/values/themes-lxx-light.xml
@@ -61,6 +61,7 @@
         <item name="keyPreviewTextColor">@color/key_text_color_lxx_light</item>
         <!-- A negative value to disable key text shadow layer. -->
         <item name="keyTextShadowRadius">-1.0</item>
+        <item name="keyPopupHintLetter"></item> <!-- No popup hint letter -->
     </style>
     <style
         name="MainKeyboardView.LXX_Light"
diff --git a/java/res/xml/key_styles_enter.xml b/java/res/xml/key_styles_enter.xml
index 50530e1..770bf38 100644
--- a/java/res/xml/key_styles_enter.xml
+++ b/java/res/xml/key_styles_enter.xml
@@ -255,6 +255,7 @@
     <!-- Enter key style -->
     <key-style
         latin:styleName="defaultEnterKeyStyle"
+        latin:keySpec="!icon/enter_key|!code/key_enter"
         latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor"
         latin:keyActionFlags="noKeyPreview"
         latin:backgroundType="action"
diff --git a/java/res/xml/key_styles_number.xml b/java/res/xml/key_styles_number.xml
index df4448c..f754b99 100644
--- a/java/res/xml/key_styles_number.xml
+++ b/java/res/xml/key_styles_number.xml
@@ -122,12 +122,4 @@
         latin:keySpec="!icon/space_key_for_number_layout|!code/key_space"
         latin:keyActionFlags="enableLongPress"
         latin:parentStyle="numKeyBaseStyle" />
-    <!-- Override defaultEnterKeyStyle in key_styles_enter.xml -->
-    <key-style
-        latin:styleName="defaultEnterKeyStyle"
-        latin:keySpec="!icon/enter_key|!code/key_enter"
-        latin:keyLabelFlags="preserveCase|autoXScale|followKeyLargeLabelRatio"
-        latin:keyActionFlags="noKeyPreview"
-        latin:backgroundType="functional"
-        latin:parentStyle="navigateMoreKeysStyle" />
 </merge>
diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
index deb6809..afbe8c8 100644
--- a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
@@ -31,6 +31,9 @@
     private static final Method METHOD_setPaddingRelative = CompatUtils.getMethod(
             View.class, "setPaddingRelative",
             int.class, int.class, int.class, int.class);
+    // Note that View.setElevation(float) has been introduced in API level 21.
+    private static final Method METHOD_setElevation = CompatUtils.getMethod(
+            View.class, "setElevation", float.class);
 
     private ViewCompatUtils() {
         // This utility class is not publicly instantiable.
@@ -51,4 +54,11 @@
         }
         CompatUtils.invoke(view, null, METHOD_setPaddingRelative, start, top, end, bottom);
     }
+
+    public static void setElevation(final View view, final float elevation) {
+        if (METHOD_setElevation == null) {
+            return;
+        }
+        CompatUtils.invoke(view, null, METHOD_setElevation, elevation);
+    }
 }
diff --git a/java/src/com/android/inputmethod/event/InputTransaction.java b/java/src/com/android/inputmethod/event/InputTransaction.java
index 5bc9111..b18bf56 100644
--- a/java/src/com/android/inputmethod/event/InputTransaction.java
+++ b/java/src/com/android/inputmethod/event/InputTransaction.java
@@ -33,7 +33,7 @@
 
     // Initial conditions
     public final SettingsValues mSettingsValues;
-    public final Event mEvent;
+    private final Event mEvent;
     public final long mTimestamp;
     public final int mSpaceState;
     public final int mShiftState;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index f967f62..5af0be6 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -29,6 +29,7 @@
 import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.NinePatchDrawable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 
@@ -48,6 +49,7 @@
  * @attr ref R.styleable#KeyboardView_spacebarBackground
  * @attr ref R.styleable#KeyboardView_spacebarIconWidthRatio
  * @attr ref R.styleable#KeyboardView_keyHintLetterPadding
+ * @attr ref R.styleable#KeyboardView_keyPopupHintLetter
  * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
  * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
  * @attr ref R.styleable#KeyboardView_keyTextShadowRadius
@@ -74,6 +76,7 @@
     // XML attributes
     private final KeyVisualAttributes mKeyVisualAttributes;
     private final float mKeyHintLetterPadding;
+    private final String mKeyPopupHintLetter;
     private final float mKeyPopupHintLetterPadding;
     private final float mKeyShiftedLetterHintPadding;
     private final float mKeyTextShadowRadius;
@@ -85,9 +88,6 @@
     private final Rect mKeyBackgroundPadding = new Rect();
     private static final float KET_TEXT_SHADOW_RADIUS_DISABLED = -1.0f;
 
-    // HORIZONTAL ELLIPSIS "...", character for popup hint.
-    private static final String POPUP_HINT_CHAR = "\u2026";
-
     // The maximum key label width in the proportion to the key width.
     private static final float MAX_LABEL_RATIO = 0.90f;
 
@@ -132,6 +132,8 @@
                 R.styleable.KeyboardView_spacebarIconWidthRatio, 1.0f);
         mKeyHintLetterPadding = keyboardViewAttr.getDimension(
                 R.styleable.KeyboardView_keyHintLetterPadding, 0.0f);
+        mKeyPopupHintLetter = keyboardViewAttr.getString(
+                R.styleable.KeyboardView_keyPopupHintLetter);
         mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
                 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0.0f);
         mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
@@ -468,6 +470,9 @@
     // Draw popup hint "..." at the bottom right corner of the key.
     protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
             final KeyDrawParams params) {
+        if (TextUtils.isEmpty(mKeyPopupHintLetter)) {
+            return;
+        }
         final int keyWidth = key.getDrawWidth();
         final int keyHeight = key.getHeight();
 
@@ -478,7 +483,7 @@
         final float hintX = keyWidth - mKeyHintLetterPadding
                 - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f;
         final float hintY = keyHeight - mKeyPopupHintLetterPadding;
-        canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
+        canvas.drawText(mKeyPopupHintLetter, hintX, hintY, paint);
     }
 
     protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x,
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 4adc28d..c7c3aaa 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -295,7 +295,8 @@
             }
         }
 
-        public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
+        public void postResetInputConnectionCaches(final boolean tryResumeSuggestions,
+                final int remainingTries) {
             removeMessages(MSG_RESET_CACHES);
             sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
                     remainingTries, null));
@@ -762,9 +763,12 @@
     private static class EditorChangeInfo {
         public final boolean mIsSameInputType;
         public final boolean mHasSameOrientation;
-        public EditorChangeInfo(final boolean isSameInputType, final boolean hasSameOrientation) {
+        public final boolean mCanReachInputConnection;
+        public EditorChangeInfo(final boolean isSameInputType, final boolean hasSameOrientation,
+                final boolean canReachInputConnection) {
             mIsSameInputType = isSameInputType;
             mHasSameOrientation = hasSameOrientation;
+            mCanReachInputConnection = canReachInputConnection;
         }
     }
 
@@ -772,16 +776,66 @@
 
     private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInput(editorInfo, restarting);
+        if (editorInfo == null) {
+            Log.e(TAG, "Null EditorInfo in onStartInput()");
+            return;
+        }
         SettingsValues currentSettingsValues = mSettings.getCurrent();
-        mLastEditorChangeInfo = new EditorChangeInfo(
-                currentSettingsValues.isSameInputType(editorInfo),
-                currentSettingsValues.hasSameOrientation(getResources().getConfiguration()));
+        final boolean isSameInputType = currentSettingsValues.isSameInputType(editorInfo);
+        final boolean hasSameOrientation =
+                currentSettingsValues.hasSameOrientation(getResources().getConfiguration());
+        mRichImm.clearSubtypeCaches();
+        final boolean inputTypeChanged = !isSameInputType;
+        final boolean isDifferentTextField = !restarting || inputTypeChanged;
+        if (isDifferentTextField || !hasSameOrientation) {
+            loadSettings();
+            currentSettingsValues = mSettings.getCurrent();
+        }
+
+        // Note: the following does a round-trip IPC on the main thread: be careful
+        final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+        final Suggest suggest = mInputLogic.mSuggest;
+        if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) {
+            // TODO: Do this automatically.
+            resetSuggest();
+        }
+        if (isDifferentTextField && currentSettingsValues.mAutoCorrectionEnabledPerUserSettings) {
+            suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
+        }
+
+        // The app calling setText() has the effect of clearing the composing
+        // span, so we should reset our state unconditionally, even if restarting is true.
+        // We also tell the input logic about the combining rules for the current subtype, so
+        // it can adjust its combiners if needed.
+        mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype());
+        // TODO[IL]: Can the following be moved to InputLogic#startInput?
+        final boolean canReachInputConnection;
+        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+                editorInfo.initialSelStart, editorInfo.initialSelEnd,
+                false /* shouldFinishComposition */)) {
+            // Sometimes, while rotating, for some reason the framework tells the app we are not
+            // connected to it and that means we can't refresh the cache. In this case, schedule a
+            // refresh later.
+            // We try resetting the caches up to 5 times before giving up.
+            mHandler.postResetInputConnectionCaches(isDifferentTextField || !hasSameOrientation,
+                    5 /* remainingTries */);
+            canReachInputConnection = false;
+        } else {
+            // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best
+            // effort to work around this bug.
+            mInputLogic.mConnection.tryFixLyingCursorPosition();
+            mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
+                    true /* shouldDelay */);
+            canReachInputConnection = true;
+        }
+
+        mLastEditorChangeInfo = new EditorChangeInfo(isSameInputType, hasSameOrientation,
+                canReachInputConnection);
     }
 
     @SuppressWarnings("deprecation")
     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInputView(editorInfo, restarting);
-        mRichImm.clearSubtypeCaches();
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         switcher.updateKeyboardTheme();
         final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
@@ -837,56 +891,13 @@
         // Note: This call should be done by InputMethodService?
         updateFullscreenMode();
 
-        // The app calling setText() has the effect of clearing the composing
-        // span, so we should reset our state unconditionally, even if restarting is true.
-        // We also tell the input logic about the combining rules for the current subtype, so
-        // it can adjust its combiners if needed.
-        mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype());
-
-        // Note: the following does a round-trip IPC on the main thread: be careful
-        final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        final Suggest suggest = mInputLogic.mSuggest;
-        if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) {
-            // TODO: Do this automatically.
-            resetSuggest();
-        }
-
-        // TODO[IL]: Can the following be moved to InputLogic#startInput?
-        final boolean canReachInputConnection;
-        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
-                editorInfo.initialSelStart, editorInfo.initialSelEnd,
-                false /* shouldFinishComposition */)) {
-            // Sometimes, while rotating, for some reason the framework tells the app we are not
-            // connected to it and that means we can't refresh the cache. In this case, schedule a
-            // refresh later.
-            // We try resetting the caches up to 5 times before giving up.
-            mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
-            // mLastSelection{Start,End} are reset later in this method, don't need to do it here
-            canReachInputConnection = false;
-        } else {
-            // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best
-            // effort to work around this bug.
-            mInputLogic.mConnection.tryFixLyingCursorPosition();
-            mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */,
-                    true /* shouldDelay */);
-            canReachInputConnection = true;
-        }
-
-        if (isDifferentTextField || !mLastEditorChangeInfo.mHasSameOrientation) {
-            loadSettings();
-        }
         final SettingsValues currentSettingsValues = mSettings.getCurrent();
         if (isDifferentTextField) {
             mainKeyboardView.closing();
 
-            if (currentSettingsValues.mAutoCorrectionEnabledPerUserSettings) {
-                suggest.setAutoCorrectionThreshold(
-                        currentSettingsValues.mAutoCorrectionThreshold);
-            }
-
             switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
                     getCurrentRecapitalizeState());
-            if (!canReachInputConnection) {
+            if (!mLastEditorChangeInfo.mCanReachInputConnection) {
                 // If we can't reach the input connection, we will call loadKeyboard again later,
                 // so we need to save its state now. The call will be done in #retryResetCaches.
                 switcher.saveKeyboardState();
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index a6b3b71..ea63cef 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin;
 
 import android.inputmethodservice.InputMethodService;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -811,4 +812,25 @@
     public boolean isCursorPositionKnown() {
         return INVALID_CURSOR_POSITION != mExpectedSelStart;
     }
+
+    /**
+     * Work around a bug that was present before Jelly Bean upon rotation.
+     *
+     * Before Jelly Bean, there is a bug where setComposingRegion and other committing
+     * functions on the input connection get ignored until the cursor moves. This method works
+     * around the bug by wiggling the cursor first, which reactivates the connection and has
+     * the subsequent methods work, then restoring it to its original position.
+     *
+     * On platforms on which this method is not present, this is a no-op.
+     */
+    public void maybeMoveTheCursorAroundAndRestoreToWorkaroundABug() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+            if (mExpectedSelStart > 0) {
+                mIC.setSelection(mExpectedSelStart - 1, mExpectedSelStart - 1);
+            } else {
+                mIC.setSelection(mExpectedSelStart + 1, mExpectedSelStart + 1);
+            }
+            mIC.setSelection(mExpectedSelStart, mExpectedSelEnd);
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 9e20abc..2be7920 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -426,12 +426,17 @@
             cancelDoubleSpacePeriodCountdown();
         }
 
-        if (processedEvent.isConsumed()) {
-            handleConsumedEvent(inputTransaction);
-        } else if (processedEvent.isFunctionalKeyEvent()) {
-            handleFunctionalEvent(inputTransaction, currentKeyboardScriptId, handler);
-        } else {
-            handleNonFunctionalEvent(inputTransaction, handler);
+        Event currentEvent = processedEvent;
+        while (null != currentEvent) {
+            if (currentEvent.isConsumed()) {
+                handleConsumedEvent(currentEvent, inputTransaction);
+            } else if (currentEvent.isFunctionalKeyEvent()) {
+                handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId,
+                        handler);
+            } else {
+                handleNonFunctionalEvent(currentEvent, inputTransaction, handler);
+            }
+            currentEvent = currentEvent.mNextEvent;
         }
         if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
                 && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
@@ -584,13 +589,14 @@
      * Consumed events represent events that have already been consumed, typically by the
      * combining chain.
      *
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleConsumedEvent(final InputTransaction inputTransaction) {
+    private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) {
         // A consumed event may have text to commit and an update to the composing state, so
         // we evaluate both. With some combiners, it's possible than an event contains both
         // and we enter both of the following if clauses.
-        final CharSequence textToCommit = inputTransaction.mEvent.getTextToCommit();
+        final CharSequence textToCommit = event.getTextToCommit();
         if (!TextUtils.isEmpty(textToCommit)) {
             mConnection.commitText(textToCommit, 1);
             inputTransaction.setDidAffectContents();
@@ -611,15 +617,15 @@
      * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
      * any key that results in multiple code points like the ".com" key.
      *
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleFunctionalEvent(final InputTransaction inputTransaction,
+    private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
             // TODO: remove these arguments
             final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
-        final Event event = inputTransaction.mEvent;
         switch (event.mKeyCode) {
             case Constants.CODE_DELETE:
-                handleBackspace(inputTransaction, currentKeyboardScriptId);
+                handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId);
                 // Backspace is a functional key, but it affects the contents of the editor.
                 inputTransaction.setDidAffectContents();
                 break;
@@ -670,11 +676,7 @@
                 // TODO: remove this object
                 final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
                         event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
-                final InputTransaction tmpTransaction = new InputTransaction(
-                        inputTransaction.mSettingsValues, tmpEvent,
-                        inputTransaction.mTimestamp, inputTransaction.mSpaceState,
-                        inputTransaction.mShiftState);
-                handleNonSpecialCharacter(tmpTransaction, handler);
+                handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
                 // Shift + Enter is treated as a functional key but it results in adding a new
                 // line, so that does affect the contents of the editor.
                 inputTransaction.setDidAffectContents();
@@ -690,12 +692,13 @@
      * These events are generally events that cause input, but in some cases they may do other
      * things like trigger an editor action.
      *
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleNonFunctionalEvent(final InputTransaction inputTransaction,
+    private void handleNonFunctionalEvent(final Event event,
+            final InputTransaction inputTransaction,
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
-        final Event event = inputTransaction.mEvent;
         inputTransaction.setDidAffectContents();
         switch (event.mCodePoint) {
             case Constants.CODE_ENTER:
@@ -718,11 +721,11 @@
                 } else {
                     // No action label, and the action from imeOptions is NONE: this is a regular
                     // enter key that should input a carriage return.
-                    handleNonSpecialCharacter(inputTransaction, handler);
+                    handleNonSpecialCharacterEvent(event, inputTransaction, handler);
                 }
                 break;
             default:
-                handleNonSpecialCharacter(inputTransaction, handler);
+                handleNonSpecialCharacterEvent(event, inputTransaction, handler);
                 break;
         }
     }
@@ -735,16 +738,18 @@
      * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
      * any key that results in multiple code points like the ".com" key.
      *
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleNonSpecialCharacter(final InputTransaction inputTransaction,
+    private void handleNonSpecialCharacterEvent(final Event event,
+            final InputTransaction inputTransaction,
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
-        final int codePoint = inputTransaction.mEvent.mCodePoint;
+        final int codePoint = event.mCodePoint;
         mSpaceState = SpaceState.NONE;
         if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
                 || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
-            handleSeparator(inputTransaction, handler);
+            handleSeparatorEvent(event, inputTransaction, handler);
         } else {
             if (SpaceState.PHANTOM == inputTransaction.mSpaceState) {
                 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
@@ -756,21 +761,23 @@
                     commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR);
                 }
             }
-            handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction);
+            handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction);
         }
     }
 
     /**
      * Handle a non-separator.
+     * @param event The event to handle.
      * @param settingsValues The current settings values.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleNonSeparator(final SettingsValues settingsValues,
+    private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues,
             final InputTransaction inputTransaction) {
-        final int codePoint = inputTransaction.mEvent.mCodePoint;
+        final int codePoint = event.mCodePoint;
         // TODO: refactor this method to stop flipping isComposingWord around all the time, and
-        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
-        // which has the same name as other handle* methods but is not the same.
+        // make it shorter (possibly cut into several pieces). Also factor
+        // handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
+        // not the same.
         boolean isComposingWord = mWordComposer.isComposingWord();
 
         // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
@@ -817,7 +824,7 @@
             resetComposingState(false /* alsoResetLastComposedWord */);
         }
         if (isComposingWord) {
-            mWordComposer.applyProcessedEvent(inputTransaction.mEvent);
+            mWordComposer.applyProcessedEvent(event);
             // If it's the first letter, make note of auto-caps state
             if (mWordComposer.isSingleLetter()) {
                 mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
@@ -825,10 +832,10 @@
             mConnection.setComposingText(getTextWithUnderline(
                     mWordComposer.getTypedWord()), 1);
         } else {
-            final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
+            final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
                     inputTransaction);
 
-            if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) {
+            if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
                 mSpaceState = SpaceState.WEAK;
             } else {
                 sendKeyCodePoint(settingsValues, codePoint);
@@ -841,12 +848,13 @@
 
     /**
      * Handle input of a separator code point.
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleSeparator(final InputTransaction inputTransaction,
+    private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction,
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
-        final int codePoint = inputTransaction.mEvent.mCodePoint;
+        final int codePoint = event.mCodePoint;
         final SettingsValues settingsValues = inputTransaction.mSettingsValues;
         final boolean wasComposingWord = mWordComposer.isComposingWord();
         // We avoid sending spaces in languages without spaces if we were composing.
@@ -872,7 +880,7 @@
             }
         }
 
-        final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
+        final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
                 inputTransaction);
 
         final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
@@ -897,10 +905,10 @@
             promotePhantomSpace(settingsValues);
         }
 
-        if (tryPerformDoubleSpacePeriod(inputTransaction)) {
+        if (tryPerformDoubleSpacePeriod(event, inputTransaction)) {
             mSpaceState = SpaceState.DOUBLE;
             inputTransaction.setRequiresUpdateSuggestions();
-        } else if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) {
+        } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
             mSpaceState = SpaceState.SWAP_PUNCTUATION;
             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
         } else if (Constants.CODE_SPACE == codePoint) {
@@ -947,12 +955,12 @@
 
     /**
      * Handle a press on the backspace key.
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      */
-    private void handleBackspace(final InputTransaction inputTransaction,
+    private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction,
             // TODO: remove this argument, put it into settingsValues
             final int currentKeyboardScriptId) {
-        final Event event = inputTransaction.mEvent;
         mSpaceState = SpaceState.NONE;
         mDeleteCount++;
 
@@ -1103,16 +1111,18 @@
      *
      * This method will check that there are two characters before the cursor and that the first
      * one is a space before it does the actual swapping.
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      * @return true if the swap has been performed, false if it was prevented by preliminary checks.
      */
-    private boolean trySwapSwapperAndSpace(final InputTransaction inputTransaction) {
+    private boolean trySwapSwapperAndSpace(final Event event,
+            final InputTransaction inputTransaction) {
         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
         if (Constants.CODE_SPACE != codePointBeforeCursor) {
             return false;
         }
         mConnection.deleteSurroundingText(1, 0);
-        final String text = inputTransaction.mEvent.getTextToCommit() + " ";
+        final String text = event.getTextToCommit() + " ";
         mConnection.commitText(text, 1);
         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
         return true;
@@ -1120,13 +1130,14 @@
 
     /*
      * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      * @return whether we should swap the space instead of removing it.
      */
-    private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(
+    private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event,
             final InputTransaction inputTransaction) {
-        final int codePoint = inputTransaction.mEvent.mCodePoint;
-        final boolean isFromSuggestionStrip = inputTransaction.mEvent.isSuggestionStripPress();
+        final int codePoint = event.mCodePoint;
+        final boolean isFromSuggestionStrip = event.isSuggestionStripPress();
         if (Constants.CODE_ENTER == codePoint &&
                 SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
             mConnection.removeTrailingSpace();
@@ -1171,14 +1182,16 @@
      * these conditions are fulfilled, this method applies the transformation and returns true.
      * Otherwise, it does nothing and returns false.
      *
+     * @param event The event to handle.
      * @param inputTransaction The transaction in progress.
      * @return true if we applied the double-space-to-period transformation, false otherwise.
      */
-    private boolean tryPerformDoubleSpacePeriod(final InputTransaction inputTransaction) {
+    private boolean tryPerformDoubleSpacePeriod(final Event event,
+            final InputTransaction inputTransaction) {
         // Check the setting, the typed character and the countdown. If any of the conditions is
         // not fulfilled, return false.
         if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod
-                || Constants.CODE_SPACE != inputTransaction.mEvent.mCodePoint
+                || Constants.CODE_SPACE != event.mCodePoint
                 || !isDoubleSpacePeriodCountdownActive(inputTransaction)) {
             return false;
         }
@@ -1424,6 +1437,7 @@
                 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
         mWordComposer.setCursorPositionWithinWord(
                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
+        mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug();
         mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor,
                 expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor());
         if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) {
@@ -2021,7 +2035,7 @@
                 mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(),
                 shouldFinishComposition)) {
             if (0 < remainingTries) {
-                handler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
+                handler.postResetInputConnectionCaches(tryResumeSuggestions, remainingTries - 1);
                 return false;
             }
             // If remainingTries is 0, we should stop waiting for new tries, however we'll still
diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
index 21f2afd..6d7f53c 100644
--- a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java
@@ -63,7 +63,6 @@
     private AlertDialog mSubtypeEnablerNotificationDialog;
     private String mSubtypePreferenceKeyForSubtypeEnabler;
 
-    private static final int MENU_ADD_SUBTYPE = Menu.FIRST;
     private static final String KEY_IS_ADDING_NEW_SUBTYPE = "is_adding_new_subtype";
     private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
             "is_subtype_enabler_notification_dialog_open";
@@ -581,14 +580,13 @@
 
     @Override
     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
-        final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style);
-        addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+        inflater.inflate(R.menu.add_style, menu);
     }
 
     @Override
     public boolean onOptionsItemSelected(final MenuItem item) {
         final int itemId = item.getItemId();
-        if (itemId == MENU_ADD_SUBTYPE) {
+        if (itemId == R.id.action_add_style) {
             final SubtypePreference newSubtype =
                     SubtypePreference.newIncompleteSubtypePreference(getActivity(), mSubtypeProxy);
             getPreferenceScreen().addPreference(newSubtype);
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 4220a95..278f2b1 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
@@ -231,30 +231,31 @@
             &probabilityEntryToWrite);
 }
 
-bool Ver4PatriciaTrieNodeWriter::addNewBigramEntry(
-        const PtNodeParams *const sourcePtNodeParams, const PtNodeParams *const targetPtNodeParam,
-        const BigramProperty *const bigramProperty, bool *const outAddedNewBigram) {
-    if (!mBigramPolicy->addNewEntry(sourcePtNodeParams->getTerminalId(),
-            targetPtNodeParam->getTerminalId(), bigramProperty, outAddedNewBigram)) {
+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());
         return false;
     }
-    if (!sourcePtNodeParams->hasBigrams()) {
+    const int ptNodePos =
+            mBuffers->getTerminalPositionLookupTable()->getTerminalPtNodePosition(prevWordIds[0]);
+    const PtNodeParams sourcePtNodeParams =
+            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(ptNodePos);
+    if (!sourcePtNodeParams.hasBigrams()) {
         // Update has bigrams flag.
-        return updatePtNodeFlags(sourcePtNodeParams->getHeadPos(),
-                sourcePtNodeParams->isBlacklisted(), sourcePtNodeParams->isNotAWord(),
-                sourcePtNodeParams->isTerminal(), sourcePtNodeParams->hasShortcutTargets(),
+        return updatePtNodeFlags(sourcePtNodeParams.getHeadPos(),
+                sourcePtNodeParams.isBlacklisted(), sourcePtNodeParams.isNotAWord(),
+                sourcePtNodeParams.isTerminal(), sourcePtNodeParams.hasShortcutTargets(),
                 true /* hasBigrams */,
-                sourcePtNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
+                sourcePtNodeParams.getCodePointCount() > 1 /* hasMultipleChars */);
     }
     return true;
 }
 
-bool Ver4PatriciaTrieNodeWriter::removeBigramEntry(
-        const PtNodeParams *const sourcePtNodeParams, const PtNodeParams *const targetPtNodeParam) {
-    return mBigramPolicy->removeEntry(sourcePtNodeParams->getTerminalId(),
-            targetPtNodeParam->getTerminalId());
+bool Ver4PatriciaTrieNodeWriter::removeNgramEntry(const WordIdArrayView prevWordIds,
+        const int wordId) {
+    return mBigramPolicy->removeEntry(prevWordIds[0], wordId);
 }
 
 bool Ver4PatriciaTrieNodeWriter::updateAllBigramEntriesAndDeleteUselessEntries(
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.h b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.h
index 08226ea..d49d9a6 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_node_writer.h
@@ -29,6 +29,7 @@
 #include "suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h"
 #include "suggest/policyimpl/dictionary/structure/pt_common/pt_node_writer.h"
 #include "suggest/policyimpl/dictionary/structure/backward/v402/content/probability_entry.h"
+#include "utils/int_array_view.h"
 
 namespace latinime {
 namespace backward {
@@ -61,8 +62,8 @@
             const PtNodeArrayReader *const ptNodeArrayReader,
             Ver4BigramListPolicy *const bigramPolicy, Ver4ShortcutListPolicy *const shortcutPolicy)
             : mTrieBuffer(trieBuffer), mBuffers(buffers), mHeaderPolicy(headerPolicy),
-              mReadingHelper(ptNodeReader, ptNodeArrayReader), mBigramPolicy(bigramPolicy),
-              mShortcutPolicy(shortcutPolicy) {}
+              mPtNodeReader(ptNodeReader), mReadingHelper(ptNodeReader, ptNodeArrayReader),
+              mBigramPolicy(bigramPolicy), mShortcutPolicy(shortcutPolicy) {}
 
     virtual ~Ver4PatriciaTrieNodeWriter() {}
 
@@ -92,12 +93,10 @@
     virtual bool writeNewTerminalPtNodeAndAdvancePosition(const PtNodeParams *const ptNodeParams,
             const UnigramProperty *const unigramProperty, int *const ptNodeWritingPos);
 
-    virtual bool addNewBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam, const BigramProperty *const bigramProperty,
-            bool *const outAddedNewBigram);
+    virtual bool addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
+            const BigramProperty *const bigramProperty, bool *const outAddedNewEntry);
 
-    virtual bool removeBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam);
+    virtual bool removeNgramEntry(const WordIdArrayView prevWordIds, const int wordId);
 
     virtual bool updateAllBigramEntriesAndDeleteUselessEntries(
             const PtNodeParams *const sourcePtNodeParams, int *const outBigramEntryCount);
@@ -135,6 +134,7 @@
     BufferWithExtendableBuffer *const mTrieBuffer;
     Ver4DictBuffers *const mBuffers;
     const HeaderPolicy *const mHeaderPolicy;
+    const PtNodeReader *const mPtNodeReader;
     DynamicPtReadingHelper mReadingHelper;
     Ver4BigramListPolicy *const mBigramPolicy;
     Ver4ShortcutListPolicy *const mShortcutPolicy;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
index baa0c0c..1296b8a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
@@ -309,8 +309,8 @@
         return false;
     }
     bool addedNewBigram = false;
-    if (mUpdatingHelper.addBigramWords(prevWordsPtNodePos[0], word1Pos, bigramProperty,
-            &addedNewBigram)) {
+    if (mUpdatingHelper.addNgramEntry(PtNodePosArrayView::fromObject(prevWordsPtNodePos),
+            word1Pos, bigramProperty, &addedNewBigram)) {
         if (addedNewBigram) {
             mBigramCount++;
         }
@@ -350,7 +350,8 @@
     if (wordPos == NOT_A_DICT_POS) {
         return false;
     }
-    if (mUpdatingHelper.removeBigramWords(prevWordsPtNodePos[0], wordPos)) {
+    if (mUpdatingHelper.removeNgramEntry(
+            PtNodePosArrayView::fromObject(prevWordsPtNodePos), wordPos)) {
         mBigramCount--;
         return true;
     } else {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
index f31c914..3c62e2e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.cpp
@@ -84,23 +84,39 @@
             unigramProperty, &pos);
 }
 
-bool DynamicPtUpdatingHelper::addBigramWords(const int word0Pos, const int word1Pos,
-        const BigramProperty *const bigramProperty, bool *const outAddedNewBigram) {
-    const PtNodeParams sourcePtNodeParams(
-            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(word0Pos));
-    const PtNodeParams targetPtNodeParams(
-            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(word1Pos));
-    return mPtNodeWriter->addNewBigramEntry(&sourcePtNodeParams, &targetPtNodeParams,
-            bigramProperty, outAddedNewBigram);
+bool DynamicPtUpdatingHelper::addNgramEntry(const PtNodePosArrayView prevWordsPtNodePos,
+        const int wordPos, const BigramProperty *const bigramProperty,
+        bool *const outAddedNewEntry) {
+    if (prevWordsPtNodePos.empty()) {
+        return false;
+    }
+    ASSERT(prevWordsPtNodePos.size() <= MAX_PREV_WORD_COUNT_FOR_N_GRAM);
+    int prevWordTerminalIds[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+    for (size_t i = 0; i < prevWordsPtNodePos.size(); ++i) {
+        prevWordTerminalIds[i] = mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(
+                prevWordsPtNodePos[i]).getTerminalId();
+    }
+    const WordIdArrayView prevWordIds(prevWordTerminalIds, prevWordsPtNodePos.size());
+    const int wordId =
+            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(wordPos).getTerminalId();
+    return mPtNodeWriter->addNgramEntry(prevWordIds, wordId, bigramProperty, outAddedNewEntry);
 }
 
-// Remove a bigram relation from word0Pos to word1Pos.
-bool DynamicPtUpdatingHelper::removeBigramWords(const int word0Pos, const int word1Pos) {
-    const PtNodeParams sourcePtNodeParams(
-            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(word0Pos));
-    const PtNodeParams targetPtNodeParams(
-            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(word1Pos));
-    return mPtNodeWriter->removeBigramEntry(&sourcePtNodeParams, &targetPtNodeParams);
+bool DynamicPtUpdatingHelper::removeNgramEntry(const PtNodePosArrayView prevWordsPtNodePos,
+        const int wordPos) {
+    if (prevWordsPtNodePos.empty()) {
+        return false;
+    }
+    ASSERT(prevWordsPtNodePos.size() <= MAX_PREV_WORD_COUNT_FOR_N_GRAM);
+    int prevWordTerminalIds[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
+    for (size_t i = 0; i < prevWordsPtNodePos.size(); ++i) {
+        prevWordTerminalIds[i] = mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(
+                prevWordsPtNodePos[i]).getTerminalId();
+    }
+    const WordIdArrayView prevWordIds(prevWordTerminalIds, prevWordsPtNodePos.size());
+    const int wordId =
+            mPtNodeReader->fetchPtNodeParamsInBufferFromPtNodePos(wordPos).getTerminalId();
+    return mPtNodeWriter->removeNgramEntry(prevWordIds, wordId);
 }
 
 bool DynamicPtUpdatingHelper::addShortcutTarget(const int wordPos,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.h b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.h
index f10d15a..97c05c1 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_updating_helper.h
@@ -19,6 +19,7 @@
 
 #include "defines.h"
 #include "suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h"
+#include "utils/int_array_view.h"
 
 namespace latinime {
 
@@ -42,12 +43,12 @@
             const int *const wordCodePoints, const int codePointCount,
             const UnigramProperty *const unigramProperty, bool *const outAddedNewUnigram);
 
-    // Add a bigram relation from word0Pos to word1Pos.
-    bool addBigramWords(const int word0Pos, const int word1Pos,
-            const BigramProperty *const bigramProperty, bool *const outAddedNewBigram);
+    // Add an n-gram entry.
+    bool addNgramEntry(const PtNodePosArrayView prevWordsPtNodePos, const int wordPos,
+            const BigramProperty *const bigramProperty, bool *const outAddedNewEntry);
 
-    // Remove a bigram relation from word0Pos to word1Pos.
-    bool removeBigramWords(const int word0Pos, const int word1Pos);
+    // Remove an n-gram entry.
+    bool removeNgramEntry(const PtNodePosArrayView prevWordsPtNodePos, const int wordPos);
 
     // Add a shortcut target.
     bool addShortcutTarget(const int wordPos, const int *const targetCodePoints,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_writer.h b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_writer.h
index a8029f7..955d779 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_writer.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_writer.h
@@ -21,6 +21,7 @@
 
 #include "defines.h"
 #include "suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h"
+#include "utils/int_array_view.h"
 
 namespace latinime {
 
@@ -70,12 +71,10 @@
     virtual bool writeNewTerminalPtNodeAndAdvancePosition(const PtNodeParams *const ptNodeParams,
             const UnigramProperty *const unigramProperty, int *const ptNodeWritingPos) = 0;
 
-    virtual bool addNewBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam, const BigramProperty *const bigramProperty,
-            bool *const outAddedNewBigram) = 0;
+    virtual bool addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
+            const BigramProperty *const bigramProperty, bool *const outAddedNewEntry) = 0;
 
-    virtual bool removeBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam) = 0;
+    virtual bool removeNgramEntry(const WordIdArrayView prevWordIds, const int wordId) = 0;
 
     virtual bool updateAllBigramEntriesAndDeleteUselessEntries(
             const PtNodeParams *const sourcePtNodeParams, int *const outBigramEntryCount) = 0;
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 07e1051..5dc91ba 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
@@ -32,11 +32,11 @@
 
 ProbabilityEntry LanguageModelDictContent::getNgramProbabilityEntry(
         const WordIdArrayView prevWordIds, const int wordId) const {
-    if (!prevWordIds.empty()) {
-        // TODO: Read n-gram entry.
+    const int bitmapEntryIndex = getBitmapEntryIndex(prevWordIds);
+    if (bitmapEntryIndex == TrieMap::INVALID_INDEX) {
         return ProbabilityEntry();
     }
-    const TrieMap::Result result = mTrieMap.getRoot(wordId);
+    const TrieMap::Result result = mTrieMap.get(wordId, bitmapEntryIndex);
     if (!result.mIsValid) {
         // Not found.
         return ProbabilityEntry();
@@ -46,14 +46,13 @@
 
 bool LanguageModelDictContent::setNgramProbabilityEntry(const WordIdArrayView prevWordIds,
         const int terminalId, const ProbabilityEntry *const probabilityEntry) {
-    if (!prevWordIds.empty()) {
-        // TODO: Add n-gram entry.
+    const int bitmapEntryIndex = getBitmapEntryIndex(prevWordIds);
+    if (bitmapEntryIndex == TrieMap::INVALID_INDEX) {
         return false;
     }
-    return mTrieMap.putRoot(terminalId, probabilityEntry->encode(mHasHistoricalInfo));
+    return mTrieMap.put(terminalId, probabilityEntry->encode(mHasHistoricalInfo), bitmapEntryIndex);
 }
 
-
 bool LanguageModelDictContent::runGCInner(
         const TerminalPositionLookupTable::TerminalIdMap *const terminalIdMap,
         const TrieMap::TrieMapRange trieMapRange,
@@ -81,4 +80,16 @@
     return true;
 }
 
+int LanguageModelDictContent::getBitmapEntryIndex(const WordIdArrayView prevWordIds) const {
+    int bitmapEntryIndex = mTrieMap.getRootBitmapEntryIndex();
+    for (const int wordId : prevWordIds) {
+        const TrieMap::Result result = mTrieMap.get(wordId, bitmapEntryIndex);
+        if (!result.mIsValid) {
+            return TrieMap::INVALID_INDEX;
+        }
+        bitmapEntryIndex = result.mNextLevelBitmapEntryIndex;
+    }
+    return bitmapEntryIndex;
+}
+
 } // namespace latinime
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 f181dfe..18f2e01 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,6 +76,8 @@
     bool runGCInner(const TerminalPositionLookupTable::TerminalIdMap *const terminalIdMap,
             const TrieMap::TrieMapRange trieMapRange, const int nextLevelBitmapEntryIndex,
             int *const outNgramCount);
+
+    int getBitmapEntryIndex(const WordIdArrayView prevWordIds) const;
 };
 } // namespace latinime
 #endif /* LATINIME_LANGUAGE_MODEL_DICT_CONTENT_H */
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 1a311b1..857222f 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
@@ -222,22 +222,19 @@
             terminalId, &probabilityEntryToWrite);
 }
 
-bool Ver4PatriciaTrieNodeWriter::addNewBigramEntry(
-        const PtNodeParams *const sourcePtNodeParams, const PtNodeParams *const targetPtNodeParam,
+bool Ver4PatriciaTrieNodeWriter::addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
         const BigramProperty *const bigramProperty, bool *const outAddedNewBigram) {
-    if (!mBigramPolicy->addNewEntry(sourcePtNodeParams->getTerminalId(),
-            targetPtNodeParam->getTerminalId(), bigramProperty, outAddedNewBigram)) {
+    if (!mBigramPolicy->addNewEntry(prevWordIds[0], wordId, bigramProperty, outAddedNewBigram)) {
         AKLOGE("Cannot add new bigram entry. terminalId: %d, targetTerminalId: %d",
-                sourcePtNodeParams->getTerminalId(), targetPtNodeParam->getTerminalId());
+                prevWordIds[0], wordId);
         return false;
     }
     return true;
 }
 
-bool Ver4PatriciaTrieNodeWriter::removeBigramEntry(
-        const PtNodeParams *const sourcePtNodeParams, const PtNodeParams *const targetPtNodeParam) {
-    return mBigramPolicy->removeEntry(sourcePtNodeParams->getTerminalId(),
-            targetPtNodeParam->getTerminalId());
+bool Ver4PatriciaTrieNodeWriter::removeNgramEntry(const WordIdArrayView prevWordIds,
+        const int wordId) {
+    return mBigramPolicy->removeEntry(prevWordIds[0], wordId);
 }
 
 bool Ver4PatriciaTrieNodeWriter::updateAllBigramEntriesAndDeleteUselessEntries(
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 162dc9b..6703dba 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
@@ -75,12 +75,10 @@
     virtual bool writeNewTerminalPtNodeAndAdvancePosition(const PtNodeParams *const ptNodeParams,
             const UnigramProperty *const unigramProperty, int *const ptNodeWritingPos);
 
-    virtual bool addNewBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam, const BigramProperty *const bigramProperty,
-            bool *const outAddedNewBigram);
+    virtual bool addNgramEntry(const WordIdArrayView prevWordIds, const int wordId,
+            const BigramProperty *const bigramProperty, bool *const outAddedNewEntry);
 
-    virtual bool removeBigramEntry(const PtNodeParams *const sourcePtNodeParams,
-            const PtNodeParams *const targetPtNodeParam);
+    virtual bool removeNgramEntry(const WordIdArrayView prevWordIds, const int wordId);
 
     virtual bool updateAllBigramEntriesAndDeleteUselessEntries(
             const PtNodeParams *const sourcePtNodeParams, int *const outBigramEntryCount);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index 2b92d5b..7238083 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -292,6 +292,7 @@
     int prevWordsPtNodePos[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
     prevWordsInfo->getPrevWordsTerminalPtNodePos(this, prevWordsPtNodePos,
             false /* tryLowerCaseSearch */);
+    const auto prevWordsPtNodePosView = PtNodePosArrayView::fromFixedSizeArray(prevWordsPtNodePos);
     // TODO: Support N-gram.
     if (prevWordsPtNodePos[0] == NOT_A_DICT_POS) {
         if (prevWordsInfo->isNthPrevWordBeginningOfSentence(1 /* n */)) {
@@ -319,10 +320,10 @@
     if (word1Pos == NOT_A_DICT_POS) {
         return false;
     }
-    bool addedNewBigram = false;
-    if (mUpdatingHelper.addBigramWords(prevWordsPtNodePos[0], word1Pos, bigramProperty,
-            &addedNewBigram)) {
-        if (addedNewBigram) {
+    bool addedNewEntry = false;
+    if (mUpdatingHelper.addNgramEntry(prevWordsPtNodePosView, word1Pos, bigramProperty,
+            &addedNewEntry)) {
+        if (addedNewEntry) {
             mBigramCount++;
         }
         return true;
@@ -352,6 +353,7 @@
     int prevWordsPtNodePos[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
     prevWordsInfo->getPrevWordsTerminalPtNodePos(this, prevWordsPtNodePos,
             false /* tryLowerCaseSerch */);
+    const auto prevWordsPtNodePosView = PtNodePosArrayView::fromFixedSizeArray(prevWordsPtNodePos);
     // TODO: Support N-gram.
     if (prevWordsPtNodePos[0] == NOT_A_DICT_POS) {
         return false;
@@ -361,7 +363,7 @@
     if (wordPos == NOT_A_DICT_POS) {
         return false;
     }
-    if (mUpdatingHelper.removeBigramWords(prevWordsPtNodePos[0], wordPos)) {
+    if (mUpdatingHelper.removeNgramEntry(prevWordsPtNodePosView, wordPos)) {
         mBigramCount--;
         return true;
     } else {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h b/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
index a294ab8..3e5c401 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
@@ -169,6 +169,10 @@
         return mBuffer.isNearSizeLimit();
     }
 
+    int getRootBitmapEntryIndex() const {
+        return ROOT_BITMAP_ENTRY_INDEX;
+    }
+
     // Returns bitmapEntryIndex. Create the next level map if it doesn't exist.
     int getNextLevelBitmapEntryIndex(const int key) {
         return getNextLevelBitmapEntryIndex(key, ROOT_BITMAP_ENTRY_INDEX);
diff --git a/native/jni/src/utils/int_array_view.h b/native/jni/src/utils/int_array_view.h
index 4bc2487..c1ddc98 100644
--- a/native/jni/src/utils/int_array_view.h
+++ b/native/jni/src/utils/int_array_view.h
@@ -56,6 +56,16 @@
     explicit IntArrayView(const std::vector<int> &vector)
             : mPtr(vector.data()), mSize(vector.size()) {}
 
+    template <int N>
+    AK_FORCE_INLINE static IntArrayView fromFixedSizeArray(const int (&array)[N]) {
+        return IntArrayView(array, N);
+    }
+
+    // Returns a view that points one int object. Does not take ownership of the given object.
+    AK_FORCE_INLINE static IntArrayView fromObject(const int *const object) {
+        return IntArrayView(object, 1);
+    }
+
     AK_FORCE_INLINE int operator[](const size_t index) const {
         ASSERT(index < mSize);
         return mPtr[index];
@@ -73,6 +83,14 @@
         return mPtr;
     }
 
+    AK_FORCE_INLINE const int *begin() const {
+        return mPtr;
+    }
+
+    AK_FORCE_INLINE const int *end() const {
+        return mPtr + mSize;
+    }
+
  private:
     DISALLOW_ASSIGNMENT_OPERATOR(IntArrayView);
 
@@ -81,6 +99,7 @@
 };
 
 using WordIdArrayView = IntArrayView;
+using PtNodePosArrayView = IntArrayView;
 
 } // namespace latinime
 #endif // LATINIME_MEMORY_VIEW_H
diff --git a/native/jni/tests/utils/int_array_view_test.cpp b/native/jni/tests/utils/int_array_view_test.cpp
index 9aa8cdc..bd843ab 100644
--- a/native/jni/tests/utils/int_array_view_test.cpp
+++ b/native/jni/tests/utils/int_array_view_test.cpp
@@ -23,16 +23,39 @@
 namespace latinime {
 namespace {
 
-TEST(MemoryViewTest, TestAccess) {
-    static const int DATA_SIZE = 10000;
-
-    std::vector<int> intVector = {3, 2, 1, 0, -1, -2};
+TEST(IntArrayViewTest, TestAccess) {
+    const std::vector<int> intVector = {3, 2, 1, 0, -1, -2};
     IntArrayView intArrayView(intVector);
     EXPECT_EQ(intVector.size(), intArrayView.size());
-    for (int i = 0; i < DATA_SIZE; ++i) {
+    for (int i = 0; i < static_cast<int>(intVector.size()); ++i) {
         EXPECT_EQ(intVector[i], intArrayView[i]);
     }
 }
 
+TEST(IntArrayViewTest, TestIteration) {
+    const std::vector<int> intVector = {3, 2, 1, 0, -1, -2};
+    IntArrayView intArrayView(intVector);
+    size_t expectedIndex = 0;
+    for (const int element : intArrayView) {
+        EXPECT_EQ(intVector[expectedIndex], element);
+        ++expectedIndex;
+    }
+    EXPECT_EQ(expectedIndex, intArrayView.size());
+}
+
+TEST(IntArrayViewTest, TestConstructFromArray) {
+    const size_t ARRAY_SIZE = 100;
+    int intArray[ARRAY_SIZE];
+    const auto intArrayView = IntArrayView::fromFixedSizeArray(intArray);
+    EXPECT_EQ(ARRAY_SIZE, intArrayView.size());
+}
+
+TEST(IntArrayViewTest, TestConstructFromObject) {
+    const int object = 10;
+    const auto intArrayView = IntArrayView::fromObject(&object);
+    EXPECT_EQ(1, intArrayView.size());
+    EXPECT_EQ(object, intArrayView[0]);
+}
+
 }  // namespace
 }  // namespace latinime