Merge "Don't resume suggestions if suggestions are not requested."
diff --git a/java/res/drawable-hdpi/sym_keyboard_info_holo_dark.png b/java/res/drawable-hdpi/sym_keyboard_info_holo_dark.png
new file mode 100644
index 0000000..f52f5af
--- /dev/null
+++ b/java/res/drawable-hdpi/sym_keyboard_info_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-hdpi/sym_keyboard_more_holo_dark.png b/java/res/drawable-hdpi/sym_keyboard_more_holo_dark.png
new file mode 100644
index 0000000..72e2b29
--- /dev/null
+++ b/java/res/drawable-hdpi/sym_keyboard_more_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-mdpi/sym_keyboard_info_holo_dark.png b/java/res/drawable-mdpi/sym_keyboard_info_holo_dark.png
new file mode 100644
index 0000000..fa4e239
--- /dev/null
+++ b/java/res/drawable-mdpi/sym_keyboard_info_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-mdpi/sym_keyboard_more_holo_dark.png b/java/res/drawable-mdpi/sym_keyboard_more_holo_dark.png
new file mode 100644
index 0000000..58e27a2
--- /dev/null
+++ b/java/res/drawable-mdpi/sym_keyboard_more_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/sym_keyboard_info_holo_dark.png b/java/res/drawable-xhdpi/sym_keyboard_info_holo_dark.png
new file mode 100644
index 0000000..513892f
--- /dev/null
+++ b/java/res/drawable-xhdpi/sym_keyboard_info_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/sym_keyboard_more_holo_dark.png b/java/res/drawable-xhdpi/sym_keyboard_more_holo_dark.png
new file mode 100644
index 0000000..07e9bee
--- /dev/null
+++ b/java/res/drawable-xhdpi/sym_keyboard_more_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_info_holo_dark.png b/java/res/drawable-xxhdpi/sym_keyboard_info_holo_dark.png
new file mode 100644
index 0000000..b214d0e
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_info_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_more_holo_dark.png b/java/res/drawable-xxhdpi/sym_keyboard_more_holo_dark.png
new file mode 100644
index 0000000..702da27
--- /dev/null
+++ b/java/res/drawable-xxhdpi/sym_keyboard_more_holo_dark.png
Binary files differ
diff --git a/java/res/layout/suggestions_strip.xml b/java/res/layout/suggestions_strip.xml
index 9c83542..9ffd231 100644
--- a/java/res/layout/suggestions_strip.xml
+++ b/java/res/layout/suggestions_strip.xml
@@ -45,4 +45,17 @@
             android:textAlignment="viewStart"
             style="?attr/suggestionWordStyle" />
     </LinearLayout>
+    <LinearLayout
+        android:id="@+id/important_notice_strip"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <TextView
+            android:id="@+id/important_notice_title"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            android:textSize="14sp"
+            style="?attr/suggestionWordStyle" />
+    </LinearLayout>
 </merge>
diff --git a/java/res/values/strings-config-important-notice.xml b/java/res/values/strings-config-important-notice.xml
index 8ee0b99..da206a3 100644
--- a/java/res/values/strings-config-important-notice.xml
+++ b/java/res/values/strings-config-important-notice.xml
@@ -20,5 +20,8 @@
 
 <resources>
     <integer name="config_important_notice_version">0</integer>
-    <!-- TODO: Add important notice title and description string resources. -->
+    <!-- The title of the important notice displayed on the suggestion strip. -->
+    <string name="important_notice_title"></string>
+    <!-- The contents of the important notice. -->
+    <string name="important_notice_contents"></string>
 </resources>
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 5fd70ea..f7eec6b 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -284,19 +284,19 @@
         int moreKeysColumn = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn);
         int value;
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = value & MORE_KEYS_COLUMN_MASK;
         }
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK);
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
         }
         mMoreKeysColumnAndFlags = moreKeysColumn;
@@ -308,7 +308,7 @@
             additionalMoreKeys = style.getStringArray(keyAttr,
                     R.styleable.Keyboard_Key_additionalMoreKeys);
         }
-        moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
+        moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         if (moreKeys != null) {
             actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
             mMoreKeys = new MoreKeySpec[moreKeys.length];
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
index 4ccecb2..dce7fc5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard.internal;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import android.text.TextUtils;
 
@@ -29,15 +30,16 @@
  * marker. An output text may consist of multiple code points separated by comma.
  * The format of the codesArray element should be:
  * <pre>
- *   codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)?
+ *   label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)?
  * </pre>
  */
 // TODO: Write unit tests for this class.
 public final class CodesArrayParser {
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final String VERTICAL_BAR_STRING = "\\|";
-    private static final String COMMA_STRING = ",";
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA);
+    private static final String VERTICAL_BAR_REGEX = // "\\|"
+            new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR });
     private static final int BASE_HEX = 16;
 
     private CodesArrayParser() {
@@ -45,7 +47,7 @@
     }
 
     private static String getLabelSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
@@ -55,7 +57,7 @@
     public static String parseLabel(final String codesArraySpec) {
         final String labelSpec = getLabelSpec(codesArraySpec);
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : labelSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : labelSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
@@ -63,17 +65,15 @@
     }
 
     private static String getCodeSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
         return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1];
     }
 
-    // codesArraySpec consists of:
-    // <label>|<code0>,<code1>,...|<minSupportSdkVersion>
     public static int getMinSupportSdkVersion(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 2) {
             return 0;
         }
@@ -98,7 +98,7 @@
             return null;
         }
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : codeSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
index 5c7c6e3..be73965 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.content.Context;
+import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
@@ -35,16 +36,19 @@
 // TODO: Implement key popup preview.
 public final class EmojiPageKeyboardView extends KeyboardView implements
         GestureDetector.OnGestureListener {
+    private static final long KEY_PRESS_DELAY_TIME = 250;  // msec
+    private static final long KEY_RELEASE_DELAY_TIME = 30;  // msec
+
     public interface OnKeyEventListener {
         public void onPressKey(Key key);
         public void onReleaseKey(Key key);
     }
 
     private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() {
-      @Override
-      public void onPressKey(final Key key) {}
-      @Override
-      public void onReleaseKey(final Key key) {}
+        @Override
+        public void onPressKey(final Key key) {}
+        @Override
+        public void onReleaseKey(final Key key) {}
     };
 
     private OnKeyEventListener mListener = EMPTY_LISTENER;
@@ -60,6 +64,7 @@
         super(context, attrs, defStyle);
         mGestureDetector = new GestureDetector(context, this);
         mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */);
+        mHandler = new Handler();
     }
 
     public void setOnKeyEventListener(final OnKeyEventListener listener) {
@@ -92,6 +97,8 @@
 
     // {@link GestureEnabler#OnGestureListener} methods.
     private Key mCurrentKey;
+    private Runnable mPendingKeyDown;
+    private final Handler mHandler;
 
     private Key getKey(final MotionEvent e) {
         final int index = e.getActionIndex();
@@ -101,6 +108,8 @@
     }
 
     public void releaseCurrentKey() {
+        mHandler.removeCallbacks(mPendingKeyDown);
+        mPendingKeyDown = null;
         final Key currentKey = mCurrentKey;
         if (currentKey == null) {
             return;
@@ -118,9 +127,17 @@
         if (key == null) {
             return false;
         }
-        key.onPressed();
-        invalidateKey(key);
-        mListener.onPressKey(key);
+        // Do not trigger key-down effect right now in case this is actually a fling action.
+        mPendingKeyDown = new Runnable() {
+            @Override
+            public void run() {
+                mPendingKeyDown = null;
+                key.onPressed();
+                invalidateKey(key);
+                mListener.onPressKey(key);
+            }
+        };
+        mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME);
         return false;
     }
 
@@ -132,13 +149,28 @@
     @Override
     public boolean onSingleTapUp(final MotionEvent e) {
         final Key key = getKey(e);
+        final Runnable pendingKeyDown = mPendingKeyDown;
+        final Key currentKey = mCurrentKey;
         releaseCurrentKey();
         if (key == null) {
             return false;
         }
-        key.onReleased();
-        invalidateKey(key);
-        mListener.onReleaseKey(key);
+        if (key == currentKey && pendingKeyDown != null) {
+            pendingKeyDown.run();
+            // Trigger key-release event a little later so that a user can see visual feedback.
+            mHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    key.onReleased();
+                    invalidateKey(key);
+                    mListener.onReleaseKey(key);
+                }
+            }, KEY_RELEASE_DELAY_TIME);
+        } else {
+            key.onReleased();
+            invalidateKey(key);
+            mListener.onReleaseKey(key);
+        }
         return true;
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index 07c5cae..c51941d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -19,106 +19,46 @@
 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
 
-import android.text.TextUtils;
-
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.StringUtils;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-
 /**
- * The string parser of more keys specification.
- * The specification is comma separated texts each of which represents one "more key".
- * The specification might have label or string resource reference in it. These references are
- * expanded before parsing comma.
- * - Label reference should be a string representation of label (!text/label_name)
- * - String resource reference should be a string representation of resource (!text/resource_name)
- * Each "more key" specification is one of the following:
- * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name)
- *   - Icon should be a string representation of icon (!icon/icon_name).
- *   - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string
- *     representation of code (!code/code_name).
+ * The string parser of the key specification.
+ *
+ * Each key specification is one of the following:
+ * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
+ * - Label optionally followed by code point (keyLabel|!code/code_name).
+ * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
+ * - Icon followed by code point (!icon/icon_name|!code/code_name).
+ * Label and keyOutputText are one of the following:
+ * - Literal string.
+ * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
+ * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
+ * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
+ * Code is one of the following:
+ * - Code point presented by hexadecimal string prefixed with "0x"
+ * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
- * Note that the '\' is also parsed by XML parser and CSV parser as well.
- * See {@link KeyboardIconsSet} about icon_name.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
  */
 public final class KeySpecParser {
-    private static final boolean DEBUG = LatinImeLogger.sDBG;
-
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final char BACKSLASH = '\\';
-    private static final char VERTICAL_BAR = '|';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
     private static final String PREFIX_HEX = "0x";
-    private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
 
     private KeySpecParser() {
         // Intentional empty constructor for utility class.
     }
 
-    /**
-     * Split the text containing multiple key specifications separated by commas into an array of
-     * key specifications.
-     * A key specification can contain a character escaped by the backslash character, including a
-     * comma character.
-     * Note that an empty key specification will be eliminated from the result array.
-     *
-     * @param text the text containing multiple key specifications.
-     * @return an array of key specification text. Null if the specified <code>text</code> is empty
-     * or has no key specifications.
-     */
-    public static String[] splitKeySpecs(final String text) {
-        if (TextUtils.isEmpty(text)) {
-            return null;
-        }
-        final int size = text.length();
-        // Optimization for one-letter key specification.
-        if (size == 1) {
-            return text.charAt(0) == COMMA ? null : new String[] { text };
-        }
-
-        ArrayList<String> list = null;
-        int start = 0;
-        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
-        // match any high or low surrogate character. So it is OK to iterate through with char
-        // index.
-        for (int pos = 0; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            if (c == COMMA) {
-                // Skip empty entry.
-                if (pos - start > 0) {
-                    if (list == null) {
-                        list = CollectionUtils.newArrayList();
-                    }
-                    list.add(text.substring(start, pos));
-                }
-                // Skip comma
-                start = pos + 1;
-            } else if (c == BACKSLASH) {
-                // Skip escape character and escaped character.
-                pos++;
-            }
-        }
-        final String remain = (size - start > 0) ? text.substring(start) : null;
-        if (list == null) {
-            return remain != null ? new String[] { remain } : null;
-        }
-        if (remain != null) {
-            list.add(remain);
-        }
-        return list.toArray(new String[list.size()]);
+    private static boolean hasIcon(final String keySpec) {
+        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
     }
 
-    private static boolean hasIcon(final String moreKeySpec) {
-        return moreKeySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
-    }
-
-    private static boolean hasCode(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
+    private static boolean hasCode(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
+        if (end > 0 && end + 1 < keySpec.length() && keySpec.startsWith(
                 KeyboardCodesSet.PREFIX_CODE, end + 1)) {
             return true;
         }
@@ -144,17 +84,17 @@
         return sb.toString();
     }
 
-    private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
-        if (moreKeySpec.indexOf(BACKSLASH, start) < 0) {
-            final int end = moreKeySpec.indexOf(VERTICAL_BAR, start);
+    private static int indexOfLabelEnd(final String keySpec, final int start) {
+        if (keySpec.indexOf(BACKSLASH, start) < 0) {
+            final int end = keySpec.indexOf(VERTICAL_BAR, start);
             if (end == 0) {
-                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec);
+                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + keySpec);
             }
             return end;
         }
-        final int length = moreKeySpec.length();
+        final int length = keySpec.length();
         for (int pos = start; pos < length; pos++) {
-            final char c = moreKeySpec.charAt(pos);
+            final char c = keySpec.charAt(pos);
             if (c == BACKSLASH && pos + 1 < length) {
                 // Skip escape char
                 pos++;
@@ -165,63 +105,63 @@
         return -1;
     }
 
-    public static String getLabel(final String moreKeySpec) {
-        if (hasIcon(moreKeySpec)) {
+    public static String getLabel(final String keySpec) {
+        if (hasIcon(keySpec)) {
             return null;
         }
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
-                : parseEscape(moreKeySpec);
-        if (TextUtils.isEmpty(label)) {
-            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+        final int end = indexOfLabelEnd(keySpec, 0);
+        final String label = (end > 0) ? parseEscape(keySpec.substring(0, end))
+                : parseEscape(keySpec);
+        if (label.isEmpty()) {
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         return label;
     }
 
-    private static String getOutputTextInternal(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
+    private static String getOutputTextInternal(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
         if (end <= 0) {
             return null;
         }
-        if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+        if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
         }
-        return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1));
+        return parseEscape(keySpec.substring(end + /* VERTICAL_BAR */1));
     }
 
-    static String getOutputText(final String moreKeySpec) {
-        if (hasCode(moreKeySpec)) {
+    public static String getOutputText(final String keySpec) {
+        if (hasCode(keySpec)) {
             return null;
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             if (StringUtils.codePointCount(outputText) == 1) {
                 // If output text is one code point, it should be treated as a code.
                 // See {@link #getCode(Resources, String)}.
                 return null;
             }
-            if (!TextUtils.isEmpty(outputText)) {
-                return outputText;
+            if (outputText.isEmpty()) {
+                throw new KeySpecParserError("Empty outputText: " + keySpec);
             }
-            throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
+            return outputText;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         if (label == null) {
-            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         // Code is automatically generated for one letter label. See {@link getCode()}.
         return (StringUtils.codePointCount(label) == 1) ? null : label;
     }
 
-    static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) {
-        if (hasCode(moreKeySpec)) {
-            final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+    public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) {
+        if (hasCode(keySpec)) {
+            final int end = indexOfLabelEnd(keySpec, 0);
+            if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
             }
-            return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
+            return parseCode(keySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             // If output text is one code point, it should be treated as a code.
             // See {@link #getOutputText(String)}.
@@ -230,7 +170,7 @@
             }
             return CODE_OUTPUT_TEXT;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         // Code is automatically generated for one letter label.
         if (StringUtils.codePointCount(label) == 1) {
             return label.codePointAt(0);
@@ -250,154 +190,22 @@
         }
     }
 
-    public static int getIconId(final String moreKeySpec) {
-        if (moreKeySpec != null && hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(
+    public static int getIconId(final String keySpec) {
+        if (keySpec != null && hasIcon(keySpec)) {
+            final int end = keySpec.indexOf(
                     VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length());
             final String name = (end < 0)
-                    ? moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
-                    : moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
+                    ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
+                    : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
-    private static String[] filterOutEmptyString(final String[] array) {
-        if (array == null) {
-            return EMPTY_STRING_ARRAY;
-        }
-        ArrayList<String> out = null;
-        for (int i = 0; i < array.length; i++) {
-            final String entry = array[i];
-            if (TextUtils.isEmpty(entry)) {
-                if (out == null) {
-                    out = CollectionUtils.arrayAsList(array, 0, i);
-                }
-            } else if (out != null) {
-                out.add(entry);
-            }
-        }
-        if (out == null) {
-            return array;
-        }
-        return out.toArray(new String[out.size()]);
-    }
-
-    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
-            final String[] additionalMoreKeySpecs) {
-        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
-        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
-        final int moreKeysCount = moreKeys.length;
-        final int additionalCount = additionalMoreKeys.length;
-        ArrayList<String> out = null;
-        int additionalIndex = 0;
-        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
-            final String moreKeySpec = moreKeys[moreKeyIndex];
-            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
-                if (additionalIndex < additionalCount) {
-                    // Replace '%' marker with additional more key specification.
-                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
-                    if (out != null) {
-                        out.add(additionalMoreKey);
-                    } else {
-                        moreKeys[moreKeyIndex] = additionalMoreKey;
-                    }
-                    additionalIndex++;
-                } else {
-                    // Filter out excessive '%' marker.
-                    if (out == null) {
-                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
-                    }
-                }
-            } else {
-                if (out != null) {
-                    out.add(moreKeySpec);
-                }
-            }
-        }
-        if (additionalCount > 0 && additionalIndex == 0) {
-            // No '%' marker is found in more keys.
-            // Insert all additional more keys to the head of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
-            for (int i = 0; i < moreKeysCount; i++) {
-                out.add(moreKeys[i]);
-            }
-        } else if (additionalIndex < additionalCount) {
-            // The number of '%' markers are less than additional more keys.
-            // Append remained additional more keys to the tail of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
-            for (int i = additionalIndex; i < additionalCount; i++) {
-                out.add(additionalMoreKeys[additionalIndex]);
-            }
-        }
-        if (out == null && moreKeysCount > 0) {
-            return moreKeys;
-        } else if (out != null && out.size() > 0) {
-            return out.toArray(new String[out.size()]);
-        } else {
-            return null;
-        }
-    }
-
     @SuppressWarnings("serial")
     public static final class KeySpecParserError extends RuntimeException {
         public KeySpecParserError(final String message) {
             super(message);
         }
     }
-
-    public static int getIntValue(final String[] moreKeys, final String key,
-            final int defaultValue) {
-        if (moreKeys == null) {
-            return defaultValue;
-        }
-        final int keyLen = key.length();
-        boolean foundValue = false;
-        int value = defaultValue;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            try {
-                if (!foundValue) {
-                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
-                    foundValue = true;
-                }
-            } catch (NumberFormatException e) {
-                throw new RuntimeException(
-                        "integer should follow after " + key + ": " + moreKeySpec);
-            }
-        }
-        return value;
-    }
-
-    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
-        if (moreKeys == null) {
-            return false;
-        }
-        boolean value = false;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            value = true;
-        }
-        return value;
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index a2242b8..7941ddd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -40,7 +40,7 @@
     protected String[] parseStringArray(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
             final String text = mTextsSet.resolveTextReference(a.getString(index));
-            return KeySpecParser.splitKeySpecs(text);
+            return MoreKeySpec.splitKeySpecs(text);
         }
         return null;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 96a5b08..f8c0988 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -47,7 +48,7 @@
  */
 public final class KeyboardTextsSet {
     public static final String PREFIX_TEXT = "!text/";
-    private static final char BACKSLASH = '\\';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
 
     // Language to texts map.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index dd58e1f..d3bc0c2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -19,10 +19,25 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Locale;
 
+/**
+ * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
+ *
+ * The more keys specification is comma separated "key specification" each of which represents one
+ * "more key".
+ * The key specification might have label or string resource reference in it. These references are
+ * expanded before parsing comma.
+ * Special character, comma ',' backslash '\' can be escaped by '\' character.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
+ */
 public final class MoreKeySpec {
     public final int mCode;
     public final String mLabel;
@@ -83,4 +98,196 @@
             return label + "|" + output;
         }
     }
+
+    private static final boolean DEBUG = LatinImeLogger.sDBG;
+    // Constants for parsing.
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final String ADDITIONAL_MORE_KEY_MARKER =
+            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
+
+    /**
+     * Split the text containing multiple key specifications separated by commas into an array of
+     * key specifications.
+     * A key specification can contain a character escaped by the backslash character, including a
+     * comma character.
+     * Note that an empty key specification will be eliminated from the result array.
+     *
+     * @param text the text containing multiple key specifications.
+     * @return an array of key specification text. Null if the specified <code>text</code> is empty
+     * or has no key specifications.
+     */
+    public static String[] splitKeySpecs(final String text) {
+        if (TextUtils.isEmpty(text)) {
+            return null;
+        }
+        final int size = text.length();
+        // Optimization for one-letter key specification.
+        if (size == 1) {
+            return text.charAt(0) == COMMA ? null : new String[] { text };
+        }
+
+        ArrayList<String> list = null;
+        int start = 0;
+        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
+        // match any high or low surrogate character. So it is OK to iterate through with char
+        // index.
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == COMMA) {
+                // Skip empty entry.
+                if (pos - start > 0) {
+                    if (list == null) {
+                        list = CollectionUtils.newArrayList();
+                    }
+                    list.add(text.substring(start, pos));
+                }
+                // Skip comma
+                start = pos + 1;
+            } else if (c == BACKSLASH) {
+                // Skip escape character and escaped character.
+                pos++;
+            }
+        }
+        final String remain = (size - start > 0) ? text.substring(start) : null;
+        if (list == null) {
+            return remain != null ? new String[] { remain } : null;
+        }
+        if (remain != null) {
+            list.add(remain);
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static String[] filterOutEmptyString(final String[] array) {
+        if (array == null) {
+            return EMPTY_STRING_ARRAY;
+        }
+        ArrayList<String> out = null;
+        for (int i = 0; i < array.length; i++) {
+            final String entry = array[i];
+            if (TextUtils.isEmpty(entry)) {
+                if (out == null) {
+                    out = CollectionUtils.arrayAsList(array, 0, i);
+                }
+            } else if (out != null) {
+                out.add(entry);
+            }
+        }
+        if (out == null) {
+            return array;
+        }
+        return out.toArray(new String[out.size()]);
+    }
+
+    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
+            final String[] additionalMoreKeySpecs) {
+        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
+        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
+        final int moreKeysCount = moreKeys.length;
+        final int additionalCount = additionalMoreKeys.length;
+        ArrayList<String> out = null;
+        int additionalIndex = 0;
+        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
+            final String moreKeySpec = moreKeys[moreKeyIndex];
+            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
+                if (additionalIndex < additionalCount) {
+                    // Replace '%' marker with additional more key specification.
+                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
+                    if (out != null) {
+                        out.add(additionalMoreKey);
+                    } else {
+                        moreKeys[moreKeyIndex] = additionalMoreKey;
+                    }
+                    additionalIndex++;
+                } else {
+                    // Filter out excessive '%' marker.
+                    if (out == null) {
+                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
+                    }
+                }
+            } else {
+                if (out != null) {
+                    out.add(moreKeySpec);
+                }
+            }
+        }
+        if (additionalCount > 0 && additionalIndex == 0) {
+            // No '%' marker is found in more keys.
+            // Insert all additional more keys to the head of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
+            for (int i = 0; i < moreKeysCount; i++) {
+                out.add(moreKeys[i]);
+            }
+        } else if (additionalIndex < additionalCount) {
+            // The number of '%' markers are less than additional more keys.
+            // Append remained additional more keys to the tail of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
+            for (int i = additionalIndex; i < additionalCount; i++) {
+                out.add(additionalMoreKeys[additionalIndex]);
+            }
+        }
+        if (out == null && moreKeysCount > 0) {
+            return moreKeys;
+        } else if (out != null && out.size() > 0) {
+            return out.toArray(new String[out.size()]);
+        } else {
+            return null;
+        }
+    }
+
+    public static int getIntValue(final String[] moreKeys, final String key,
+            final int defaultValue) {
+        if (moreKeys == null) {
+            return defaultValue;
+        }
+        final int keyLen = key.length();
+        boolean foundValue = false;
+        int value = defaultValue;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            try {
+                if (!foundValue) {
+                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
+                    foundValue = true;
+                }
+            } catch (NumberFormatException e) {
+                throw new RuntimeException(
+                        "integer should follow after " + key + ": " + moreKeySpec);
+            }
+        }
+        return value;
+    }
+
+    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
+        if (moreKeys == null) {
+            return false;
+        }
+        boolean value = false;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            value = true;
+        }
+        return value;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 95823da..00eb57c 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.makedict.Word;
 import com.android.inputmethod.latin.settings.NativeSuggestOptions;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
@@ -68,11 +69,12 @@
     private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2;
     private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
 
-    // Format to get unigram historical info from native side via getWordPropertyNative().
-    private static final int FORMAT_WORD_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT = 3;
-    private static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 0;
-    private static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 1;
-    private static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 2;
+    // Format to get probability and historical info from native side via getWordPropertyNative().
+    public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4;
+    public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0;
+    public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1;
+    public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2;
+    public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3;
 
     private long mNativeDict;
     private final Locale mLocale;
@@ -144,9 +146,10 @@
     private static native int getProbabilityNative(long dict, int[] word);
     private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
     private static native void getWordPropertyNative(long dict, int[] word,
-            int[] outCodePoints, boolean[] outFlags, int[] outProbability,
-            int[] outHistoricalInfo, ArrayList<int[]> outShortcutTargets,
-            ArrayList<Integer> outShortcutProbabilities);
+            int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo,
+            ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo,
+            ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities);
+    private static native int getNextWordNative(long dict, int token, int[] outCodePoints);
     private static native int getSuggestionsNative(long dict, long proximityInfo,
             long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
             int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint,
@@ -313,22 +316,49 @@
         final int[] codePoints = StringUtils.toCodePointArray(word);
         final int[] outCodePoints = new int[MAX_WORD_LENGTH];
         final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT];
-        final int[] outProbability = new int[1];
-        final int[] outHistoricalInfo =
-                new int[FORMAT_WORD_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT];
+        final int[] outProbabilityInfo =
+                new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT];
+        final ArrayList<int[]> outBigramTargets = CollectionUtils.newArrayList();
+        final ArrayList<int[]> outBigramProbabilityInfo = CollectionUtils.newArrayList();
         final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList();
         final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList();
-        getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbability,
-                outHistoricalInfo, outShortcutTargets, outShortcutProbabilities);
+        getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbabilityInfo,
+                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+                outShortcutProbabilities);
         return new WordProperty(codePoints,
                 outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX],
                 outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX],
                 outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX],
-                outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outProbability[0],
-                outHistoricalInfo[FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX],
-                outHistoricalInfo[FORMAT_WORD_PROPERTY_LEVEL_INDEX],
-                outHistoricalInfo[FORMAT_WORD_PROPERTY_COUNT_INDEX],
-                outShortcutTargets, outShortcutProbabilities);
+                outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outProbabilityInfo,
+                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+                outShortcutProbabilities);
+    }
+
+    public static class GetNextWordPropertyResult {
+        public WordProperty mWordProperty;
+        public int mNextToken;
+
+        public GetNextWordPropertyResult(final WordProperty wordPreperty, final int nextToken) {
+            mWordProperty = wordPreperty;
+            mNextToken = nextToken;
+        }
+    }
+
+    /**
+     * Method to iterate all words in the dictionary for makedict.
+     * If token is 0, this method newly starts iterating the dictionary.
+     */
+    @UsedForTesting
+    public GetNextWordPropertyResult getNextWordProperty(final int token) {
+        final int[] codePoints = new int[MAX_WORD_LENGTH];
+        final int nextToken = getNextWordNative(mNativeDict, token, codePoints);
+        int len = 0;
+        // codePoints is null-terminated if its length is shorter than the array length.
+        while (len < MAX_WORD_LENGTH && codePoints[len] != 0) {
+            ++len;
+        }
+        final String word = new String(mOutputCodePoints, 0, len);
+        return new GetNextWordPropertyResult(getWordProperty(word), nextToken);
     }
 
     // Add a unigram entry to binary dictionary with unigram attributes in native code.
@@ -379,7 +409,6 @@
                 return;
             }
         }
-
     }
 
     private void reopen() {
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 0b396b1..d6ac71f 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -191,6 +191,8 @@
     public static final int CODE_QUESTION_MARK = '?';
     public static final int CODE_EXCLAMATION_MARK = '!';
     public static final int CODE_SLASH = '/';
+    public static final int CODE_BACKSLASH = '\\';
+    public static final int CODE_VERTICAL_BAR = '|';
     public static final int CODE_COMMERCIAL_AT = '@';
     public static final int CODE_PLUS = '+';
     public static final int CODE_PERCENT = '%';
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 8f4b2d6..2c7ef70 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1157,6 +1157,13 @@
         mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit);
     }
 
+    // Callback for the {@link SuggestionStripView}, to call when the important notice strip is
+    // pressed.
+    @Override
+    public void showImportantNoticeContents() {
+        // TODO: Show dialog to display important notice contents.
+    }
+
     public void displaySettingsDialog() {
         if (isShowingOptionDialog()) return;
         showSubtypeSelectorAndSettings();
@@ -1401,10 +1408,12 @@
             // because it may differ from mWordComposer.mTypedWord.
             autoCorrection = sourceSuggestedWords.mTypedWord;
         }
-        if (SuggestedWords.EMPTY != suggestedWords) {
+        if (SuggestedWords.EMPTY == suggestedWords) {
+            setNeutralSuggestionStrip();
+        } else {
             mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
+            setSuggestedWords(suggestedWords, isSuggestionsStripVisible());
         }
-        setSuggestedWords(suggestedWords, isSuggestionsStripVisible());
         // Cache the auto-correction in accessibility code so we can speak it if the user
         // touches a key that will insert it.
         AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords,
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 8ba32ff..70cb2b2 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -19,6 +19,7 @@
 import android.content.res.Resources;
 
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.R;
@@ -55,7 +56,7 @@
                 res.getString(R.string.symbols_word_connectors));
         mSortedWordSeparators = StringUtils.toSortedCodePointArray(
                 res.getString(R.string.symbols_word_separators));
-        final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
+        final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs(res.getString(
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 5ed42ab..da084e1 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -28,7 +28,6 @@
 import android.graphics.Typeface;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
-import android.support.v4.view.GravityCompat;
 import android.support.v4.view.ViewCompat;
 import android.text.Spannable;
 import android.text.SpannableString;
@@ -45,6 +44,7 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.inputmethod.compat.TextViewCompatUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
@@ -492,7 +492,24 @@
                 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
     }
 
-    private static void setLayoutWeight(final View v, final float weight, final int height) {
+    public void layoutImportantNotice(final View importantNoticeStrip, final int stripWidth) {
+        final Resources res = importantNoticeStrip.getResources();
+        final Drawable infoIcon = res.getDrawable(R.drawable.sym_keyboard_info_holo_dark);
+        final Drawable moreIcon = res.getDrawable(R.drawable.sym_keyboard_more_holo_dark);
+        final int width = stripWidth - infoIcon.getIntrinsicWidth() - moreIcon.getIntrinsicWidth();
+        final TextView titleView = (TextView)importantNoticeStrip.findViewById(
+                R.id.important_notice_title);
+        titleView.setTextColor(mColorAutoCorrect);
+        TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                titleView, infoIcon, null, moreIcon, null);
+        final CharSequence importantNoticeTitle = res.getText(R.string.important_notice_title);
+        titleView.setTextScaleX(1.0f); // Reset textScaleX.
+        final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint());
+        titleView.setText(importantNoticeTitle);
+        titleView.setTextScaleX(titleScaleX);
+    }
+
+    static void setLayoutWeight(final View v, final float weight, final int height) {
         final ViewGroup.LayoutParams lp = v.getLayoutParams();
         if (lp instanceof LinearLayout.LayoutParams) {
             final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 22804fc..fe95d67 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -54,6 +54,7 @@
         OnLongClickListener {
     public interface Listener {
         public void addWordToUserDictionary(String word);
+        public void showImportantNoticeContents();
         public void pickSuggestionManually(int index, SuggestedWordInfo word);
     }
 
@@ -62,6 +63,7 @@
 
     private final ViewGroup mSuggestionsStrip;
     private final ViewGroup mAddToDictionaryStrip;
+    private final View mImportantNoticeStrip;
     MainKeyboardView mMainKeyboardView;
 
     private final View mMoreSuggestionsContainer;
@@ -81,10 +83,13 @@
     private static class StripVisibilityGroup {
         private final View mSuggestionsStrip;
         private final View mAddToDictionaryStrip;
+        private final View mImportantNoticeStrip;
 
-        public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip) {
+        public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip,
+                final View importantNoticeStrip) {
             mSuggestionsStrip = suggestionsStrip;
             mAddToDictionaryStrip = addToDictionaryStrip;
+            mImportantNoticeStrip = importantNoticeStrip;
             showSuggestionsStrip();
         }
 
@@ -93,16 +98,25 @@
                     : ViewCompat.LAYOUT_DIRECTION_LTR;
             ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection);
             ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection);
+            ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection);
         }
 
         public void showSuggestionsStrip() {
             mSuggestionsStrip.setVisibility(VISIBLE);
             mAddToDictionaryStrip.setVisibility(INVISIBLE);
+            mImportantNoticeStrip.setVisibility(INVISIBLE);
         }
 
         public void showAddToDictionaryStrip() {
             mSuggestionsStrip.setVisibility(INVISIBLE);
             mAddToDictionaryStrip.setVisibility(VISIBLE);
+            mImportantNoticeStrip.setVisibility(INVISIBLE);
+        }
+
+        public void showImportantNoticeStrip() {
+            mSuggestionsStrip.setVisibility(INVISIBLE);
+            mAddToDictionaryStrip.setVisibility(INVISIBLE);
+            mImportantNoticeStrip.setVisibility(VISIBLE);
         }
 
         public boolean isShowingAddToDictionaryStrip() {
@@ -128,7 +142,9 @@
 
         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
         mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip);
-        mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip);
+        mImportantNoticeStrip = findViewById(R.id.important_notice_strip);
+        mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip,
+                mImportantNoticeStrip);
 
         for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) {
             final TextView word = new TextView(context, null, R.attr.suggestionWordStyle);
@@ -176,6 +192,7 @@
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
         }
+        mStripVisibilityGroup.showSuggestionsStrip();
     }
 
     public int setMoreSuggestionsHeight(final int remainingHeight) {
@@ -203,6 +220,12 @@
         return false;
     }
 
+    public void showImportantNoticeTitle() {
+        mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, getWidth());
+        mStripVisibilityGroup.showImportantNoticeStrip();
+        mImportantNoticeStrip.setOnClickListener(this);
+    }
+
     public void clear() {
         mSuggestionsStrip.removeAllViews();
         removeAllDebugInfoViews();
@@ -360,6 +383,10 @@
 
     @Override
     public void onClick(final View view) {
+        if (view == mImportantNoticeStrip) {
+            mListener.showImportantNoticeContents();
+            return;
+        }
         final Object tag = view.getTag();
         // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}.
         if (tag instanceof String) {
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
index 36b927e..b18a1d8 100644
--- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.utils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 
 import java.util.ArrayList;
 
@@ -57,9 +58,9 @@
 
     // Note that none of these characters match high or low surrogate characters, so we need not
     // take care of matching by code point.
-    private static final char COMMA = ',';
-    private static final char SPACE = ' ';
-    private static final char QUOTE = '"';
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char SPACE = Constants.CODE_SPACE;
+    private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE;
 
     @SuppressWarnings("serial")
     public static class CsvParseException extends RuntimeException {
diff --git a/java/src/com/android/inputmethod/latin/utils/WordProperty.java b/java/src/com/android/inputmethod/latin/utils/WordProperty.java
index d6c0f90..ba9b114 100644
--- a/java/src/com/android/inputmethod/latin/utils/WordProperty.java
+++ b/java/src/com/android/inputmethod/latin/utils/WordProperty.java
@@ -32,15 +32,30 @@
     public final boolean mIsBlacklisted;
     public final boolean mHasBigrams;
     public final boolean mHasShortcuts;
-    public final int mProbability;
-    // mTimestamp, mLevel and mCount are historical info. These values are depend on the
-    // implementation in native code; thus, we must not use them and have any assumptions about
-    // them except for tests.
-    public final int mTimestamp;
-    public final int mLevel;
-    public final int mCount;
+    public final ProbabilityInfo mProbabilityInfo;
+    public final ArrayList<WeightedString> mBigramTargets = CollectionUtils.newArrayList();
+    public final ArrayList<ProbabilityInfo> mBigramProbabilityInfo = CollectionUtils.newArrayList();
     public final ArrayList<WeightedString> mShortcutTargets = CollectionUtils.newArrayList();
 
+    // TODO: Use this kind of Probability class for dictionary read/write code under the makedict
+    // package.
+    public static final class ProbabilityInfo {
+        public final int mProbability;
+        // wTimestamp, mLevel and mCount are historical info. These values are depend on the
+        // implementation in native code; thus, we must not use them and have any assumptions about
+        // them except for tests.
+        public final int mTimestamp;
+        public final int mLevel;
+        public final int mCount;
+
+        public ProbabilityInfo(final int[] probabilityInfo) {
+            mProbability = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX];
+            mTimestamp = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX];
+            mLevel = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX];
+            mCount = probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX];
+        }
+    }
+
     private static int getCodePointCount(final int[] codePoints) {
         for (int i = 0; i < codePoints.length; i++) {
             if (codePoints[i] == 0) {
@@ -53,18 +68,29 @@
     // This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY.
     public WordProperty(final int[] codePoints, final boolean isNotAWord,
             final boolean isBlacklisted, final boolean hasBigram,
-            final boolean hasShortcuts, final int probability, final int timestamp,
-            final int level, final int count, final ArrayList<int[]> shortcutTargets,
+            final boolean hasShortcuts, final int[] probabilityInfo,
+            final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo,
+            final ArrayList<int[]> shortcutTargets,
             final ArrayList<Integer> shortcutProbabilities) {
         mCodePoints = new String(codePoints, 0 /* offset */, getCodePointCount(codePoints));
         mIsNotAWord = isNotAWord;
         mIsBlacklisted = isBlacklisted;
         mHasBigrams = hasBigram;
         mHasShortcuts = hasShortcuts;
-        mProbability = probability;
-        mTimestamp = timestamp;
-        mLevel = level;
-        mCount = count;
+        mProbabilityInfo = new ProbabilityInfo(probabilityInfo);
+
+        final int bigramTargetCount = bigramTargets.size();
+        for (int i = 0; i < bigramTargetCount; i++) {
+            final int[] bigramTargetCodePointArray = bigramTargets.get(i);
+            final String bigramTargetString = new String(bigramTargetCodePointArray,
+                    0 /* offset */, getCodePointCount(bigramTargetCodePointArray));
+            final ProbabilityInfo bigramProbability =
+                    new ProbabilityInfo(bigramProbabilityInfo.get(i));
+            mBigramTargets.add(
+                    new WeightedString(bigramTargetString, bigramProbability.mProbability));
+            mBigramProbabilityInfo.add(bigramProbability);
+        }
+
         final int shortcutTargetCount = shortcutTargets.size();
         for (int i = 0; i < shortcutTargetCount; i++) {
             final int[] shortcutTargetCodePointArray = shortcutTargets.get(i);
@@ -77,6 +103,6 @@
 
     @UsedForTesting
     public boolean isValid() {
-        return mProbability != BinaryDictionary.NOT_A_PROBABILITY;
+        return mProbabilityInfo.mProbability != BinaryDictionary.NOT_A_PROBABILITY;
     }
 }
\ No newline at end of file
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 16a3fe8..c919ebd 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -260,18 +260,39 @@
             word1Length);
 }
 
+// Method to iterate all words in the dictionary for makedict.
+// If token is 0, this method newly starts iterating the dictionary. This method returns 0 when
+// the dictionary does not have a next word.
+static jint latinime_BinaryDictionary_getNextWord(JNIEnv *env, jclass clazz,
+        jlong dict, jint token, jintArray outCodePoints) {
+    Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
+    if (!dictionary) return 0;
+    const jsize outCodePointsLength = env->GetArrayLength(outCodePoints);
+    if (outCodePointsLength != MAX_WORD_LENGTH) {
+        AKLOGE("Invalid outCodePointsLength: %d", outCodePointsLength);
+        ASSERT(false);
+        return 0;
+    }
+    int wordCodePoints[outCodePointsLength];
+    memset(wordCodePoints, 0, sizeof(wordCodePoints));
+    const int nextToken = dictionary->getNextWordAndNextToken(token, wordCodePoints);
+    env->SetIntArrayRegion(outCodePoints, 0, outCodePointsLength, wordCodePoints);
+    return nextToken;
+}
+
 static void latinime_BinaryDictionary_getWordProperty(JNIEnv *env, jclass clazz,
         jlong dict, jintArray word, jintArray outCodePoints, jbooleanArray outFlags,
-        jintArray outProbability, jintArray outHistoricalInfo, jobject outShortcutTargets,
-        jobject outShortcutProbabilities) {
+        jintArray outProbabilityInfo, jobject outBigramTargets, jobject outBigramProbabilityInfo,
+        jobject outShortcutTargets, jobject outShortcutProbabilities) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return;
     const jsize wordLength = env->GetArrayLength(word);
     int wordCodePoints[wordLength];
     env->GetIntArrayRegion(word, 0, wordLength, wordCodePoints);
     const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints, wordLength);
-    wordProperty.outputProperties(env, outCodePoints, outFlags, outProbability,
-            outHistoricalInfo, outShortcutTargets, outShortcutProbabilities);
+    wordProperty.outputProperties(env, outCodePoints, outFlags, outProbabilityInfo,
+            outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+            outShortcutProbabilities);
 }
 
 static jfloat latinime_BinaryDictionary_calcNormalizedScore(JNIEnv *env, jclass clazz,
@@ -521,10 +542,16 @@
     },
     {
         const_cast<char *>("getWordPropertyNative"),
-        const_cast<char *>("(J[I[I[Z[I[ILjava/util/ArrayList;Ljava/util/ArrayList;)V"),
+        const_cast<char *>("(J[I[I[Z[ILjava/util/ArrayList;Ljava/util/ArrayList;"
+                "Ljava/util/ArrayList;Ljava/util/ArrayList;)V"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_getWordProperty)
     },
     {
+        const_cast<char *>("getNextWordNative"),
+        const_cast<char *>("(JI[I)I"),
+        reinterpret_cast<void *>(latinime_BinaryDictionary_getNextWord)
+    },
+    {
         const_cast<char *>("calcNormalizedScoreNative"),
         const_cast<char *>("([I[II)F"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_calcNormalizedScore)
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index 16b1a56..9b71eff 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -150,6 +150,12 @@
             codePoints, codePointCount);
 }
 
+int Dictionary::getNextWordAndNextToken(const int token, int *const outCodePoints) {
+    TimeKeeper::setCurrentTime();
+    return mDictionaryStructureWithBufferPolicy.get()->getNextWordAndNextToken(
+            token, outCodePoints);
+}
+
 void Dictionary::logDictionaryInfo(JNIEnv *const env) const {
     int dictionaryIdCodePointBuffer[HEADER_ATTRIBUTE_BUFFER_SIZE];
     int versionStringCodePointBuffer[HEADER_ATTRIBUTE_BUFFER_SIZE];
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index 4a468f3..0a413cb 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -96,6 +96,11 @@
 
     const WordProperty getWordProperty(const int *const codePoints, const int codePointCount);
 
+    // Method to iterate all words in the dictionary.
+    // The returned token has to be used to get the next word. If token is 0, this method newly
+    // starts iterating the dictionary.
+    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const {
         return mDictionaryStructureWithBufferPolicy.get();
     }
diff --git a/native/jni/src/suggest/core/dictionary/word_property.cpp b/native/jni/src/suggest/core/dictionary/word_property.cpp
index ed32bde..288e6b0 100644
--- a/native/jni/src/suggest/core/dictionary/word_property.cpp
+++ b/native/jni/src/suggest/core/dictionary/word_property.cpp
@@ -19,29 +19,53 @@
 namespace latinime {
 
 void WordProperty::outputProperties(JNIEnv *const env, jintArray outCodePoints,
-        jbooleanArray outFlags, jintArray outProbability, jintArray outHistoricalInfo,
-        jobject outShortcutTargets, jobject outShortcutProbabilities) const {
+        jbooleanArray outFlags, jintArray outProbabilityInfo, jobject outBigramTargets,
+        jobject outBigramProbabilities, jobject outShortcutTargets,
+        jobject outShortcutProbabilities) const {
     env->SetIntArrayRegion(outCodePoints, 0 /* start */, mCodePoints.size(), &mCodePoints[0]);
     jboolean flags[] = {mIsNotAWord, mIsBlacklisted, mHasBigrams, mHasShortcuts};
     env->SetBooleanArrayRegion(outFlags, 0 /* start */, NELEMS(flags), flags);
-    env->SetIntArrayRegion(outProbability, 0 /* start */, 1 /* len */, &mProbability);
-    int historicalInfo[] = {mTimestamp, mLevel, mCount};
-    env->SetIntArrayRegion(outHistoricalInfo, 0 /* start */, NELEMS(historicalInfo),
-            historicalInfo);
+    int probabilityInfo[] = {mProbability, mTimestamp, mLevel, mCount};
+    env->SetIntArrayRegion(outProbabilityInfo, 0 /* start */, NELEMS(probabilityInfo),
+            probabilityInfo);
 
     jclass integerClass = env->FindClass("java/lang/Integer");
     jmethodID intToIntegerConstructorId = env->GetMethodID(integerClass, "<init>", "(I)V");
     jclass arrayListClass = env->FindClass("java/util/ArrayList");
     jmethodID addMethodId = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
-    const int shortcutTargetCount = mShortcutTargets.size();
+
+    // Output bigrams.
+    const int bigramCount = mBigrams.size();
+    for (int i = 0; i < bigramCount; ++i) {
+        const BigramProperty *const bigramProperty = &mBigrams[i];
+        const std::vector<int> *const word1CodePoints = bigramProperty->getTargetCodePoints();
+        jintArray bigramWord1CodePointArray = env->NewIntArray(word1CodePoints->size());
+        env->SetIntArrayRegion(bigramWord1CodePointArray, 0 /* start */,
+                word1CodePoints->size(), &word1CodePoints->at(0));
+        env->CallVoidMethod(outBigramTargets, addMethodId, bigramWord1CodePointArray);
+        env->DeleteLocalRef(bigramWord1CodePointArray);
+
+        int bigramProbabilityInfo[] = {bigramProperty->getProbability(),
+                bigramProperty->getTimestamp(), bigramProperty->getLevel(),
+                bigramProperty->getCount()};
+        jintArray bigramProbabilityInfoArray = env->NewIntArray(NELEMS(bigramProbabilityInfo));
+        env->SetIntArrayRegion(bigramProbabilityInfoArray, 0 /* start */,
+                NELEMS(bigramProbabilityInfo), bigramProbabilityInfo);
+        env->CallVoidMethod(outBigramProbabilities, addMethodId, bigramProbabilityInfoArray);
+        env->DeleteLocalRef(bigramProbabilityInfoArray);
+    }
+
+    // Output shortcuts.
+    const int shortcutTargetCount = mShortcuts.size();
     for (int i = 0; i < shortcutTargetCount; ++i) {
-        jintArray shortcutTargetCodePointArray = env->NewIntArray(mShortcutTargets[i].size());
+        const std::vector<int> *const targetCodePoints = mShortcuts[i].getTargetCodePoints();
+        jintArray shortcutTargetCodePointArray = env->NewIntArray(targetCodePoints->size());
         env->SetIntArrayRegion(shortcutTargetCodePointArray, 0 /* start */,
-                mShortcutTargets[i].size(), &mShortcutTargets[i][0]);
+                targetCodePoints->size(), &targetCodePoints->at(0));
         env->CallVoidMethod(outShortcutTargets, addMethodId, shortcutTargetCodePointArray);
         env->DeleteLocalRef(shortcutTargetCodePointArray);
         jobject integerProbability = env->NewObject(integerClass, intToIntegerConstructorId,
-                mShortcutProbabilities[i]);
+                mShortcuts[i].getProbability());
         env->CallVoidMethod(outShortcutProbabilities, addMethodId, integerProbability);
         env->DeleteLocalRef(integerProbability);
     }
diff --git a/native/jni/src/suggest/core/dictionary/word_property.h b/native/jni/src/suggest/core/dictionary/word_property.h
index dcac853..40b1a91 100644
--- a/native/jni/src/suggest/core/dictionary/word_property.h
+++ b/native/jni/src/suggest/core/dictionary/word_property.h
@@ -28,27 +28,78 @@
 // This class is used for returning information belonging to a word to java side.
 class WordProperty {
  public:
-    // TODO: Add bigram information.
+    class BigramProperty {
+     public:
+        BigramProperty(const std::vector<int> *const targetCodePoints,
+                const int probability, const int timestamp, const int level, const int count)
+                : mTargetCodePoints(*targetCodePoints), mProbability(probability),
+                  mTimestamp(timestamp), mLevel(level), mCount(count) {}
+
+        const std::vector<int> *getTargetCodePoints() const {
+            return &mTargetCodePoints;
+        }
+
+        int getProbability() const {
+            return mProbability;
+        }
+
+        int getTimestamp() const {
+            return mTimestamp;
+        }
+
+        int getLevel() const {
+            return mLevel;
+        }
+
+        int getCount() const {
+            return mCount;
+        }
+
+     private:
+        std::vector<int> mTargetCodePoints;
+        int mProbability;
+        int mTimestamp;
+        int mLevel;
+        int mCount;
+    };
+
+    class ShortcutProperty {
+     public:
+        ShortcutProperty(const std::vector<int> *const targetCodePoints, const int probability)
+                : mTargetCodePoints(*targetCodePoints), mProbability(probability) {}
+
+        const std::vector<int> *getTargetCodePoints() const {
+            return &mTargetCodePoints;
+        }
+
+        int getProbability() const {
+            return mProbability;
+        }
+
+     private:
+        std::vector<int> mTargetCodePoints;
+        int mProbability;
+    };
+
     // Invalid word.
     WordProperty()
             : mCodePoints(), mIsNotAWord(false), mIsBlacklisted(false),
               mHasBigrams(false), mHasShortcuts(false), mProbability(NOT_A_PROBABILITY),
-              mTimestamp(0), mLevel(0), mCount(0), mShortcutTargets(), mShortcutProbabilities() {}
+              mTimestamp(0), mLevel(0), mCount(0), mBigrams(), mShortcuts() {}
 
     WordProperty(const std::vector<int> *const codePoints,
             const bool isNotAWord, const bool isBlacklisted, const bool hasBigrams,
             const bool hasShortcuts, const int probability, const int timestamp,
-            const int level, const int count,
-            const std::vector<std::vector<int> > *const shortcutTargets,
-            const std::vector<int> *const shortcutProbabilities)
+            const int level, const int count, const std::vector<BigramProperty> *const bigrams,
+            const std::vector<ShortcutProperty> *const shortcuts)
             : mCodePoints(*codePoints), mIsNotAWord(isNotAWord), mIsBlacklisted(isBlacklisted),
               mHasBigrams(hasBigrams), mHasShortcuts(hasShortcuts), mProbability(probability),
-              mTimestamp(timestamp), mLevel(level), mCount(count),
-              mShortcutTargets(*shortcutTargets), mShortcutProbabilities(*shortcutProbabilities) {}
+              mTimestamp(timestamp), mLevel(level), mCount(count), mBigrams(*bigrams),
+              mShortcuts(*shortcuts) {}
 
     void outputProperties(JNIEnv *const env, jintArray outCodePoints, jbooleanArray outFlags,
-            jintArray outProbability, jintArray outHistoricalInfo, jobject outShortcutTargets,
-            jobject outShortcutProbabilities) const;
+            jintArray outProbabilityInfo, jobject outBigramTargets, jobject outBigramProbabilities,
+            jobject outShortcutTargets, jobject outShortcutProbabilities) const;
 
  private:
     DISALLOW_ASSIGNMENT_OPERATOR(WordProperty);
@@ -63,9 +114,8 @@
     int mTimestamp;
     int mLevel;
     int mCount;
-    // Shortcut
-    std::vector<std::vector<int> > mShortcutTargets;
-    std::vector<int> mShortcutProbabilities;
+    std::vector<BigramProperty> mBigrams;
+    std::vector<ShortcutProperty> mShortcuts;
 };
 } // namespace latinime
 #endif // LATINIME_WORD_PROPERTY_H
diff --git a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
index b878984..7844195 100644
--- a/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_structure_with_buffer_policy.h
@@ -95,6 +95,11 @@
     virtual const WordProperty getWordProperty(const int *const codePonts,
             const int codePointCount) const = 0;
 
+    // Method to iterate all words in the dictionary.
+    // The returned token has to be used to get the next word. If token is 0, this method newly
+    // starts iterating the dictionary.
+    virtual int getNextWordAndNextToken(const int token, int *const outCodePoints) = 0;
+
  protected:
     DictionaryStructureWithBufferPolicy() {}
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
index 67d615e..319c815 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h
@@ -129,6 +129,11 @@
         return WordProperty();
     }
 
+    int getNextWordAndNextToken(const int token, int *const outCodePoints) {
+        // getNextWordAndNextToken is not supported.
+        return 0;
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(PatriciaTriePolicy);
 
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 0b067e1..1c420e0 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
@@ -332,9 +332,44 @@
             mBuffers.get()->getProbabilityDictContent()->getProbabilityEntry(
                     ptNodeParams.getTerminalId());
     const HistoricalInfo *const historicalInfo = probabilityEntry.getHistoricalInfo();
+    // Fetch bigram information.
+    std::vector<WordProperty::BigramProperty> bigrams;
+    const int bigramListPos = getBigramsPositionOfPtNode(ptNodePos);
+    if (bigramListPos != NOT_A_DICT_POS) {
+        int bigramWord1CodePoints[MAX_WORD_LENGTH];
+        const BigramDictContent *const bigramDictContent = mBuffers.get()->getBigramDictContent();
+        const TerminalPositionLookupTable *const terminalPositionLookupTable =
+                mBuffers.get()->getTerminalPositionLookupTable();
+        bool hasNext = true;
+        int readingPos = bigramListPos;
+        while (hasNext) {
+            const BigramEntry bigramEntry =
+                    bigramDictContent->getBigramEntryAndAdvancePosition(&readingPos);
+            hasNext = bigramEntry.hasNext();
+            const int word1TerminalId = bigramEntry.getTargetTerminalId();
+            const int word1TerminalPtNodePos =
+                    terminalPositionLookupTable->getTerminalPtNodePosition(word1TerminalId);
+            if (word1TerminalPtNodePos == NOT_A_DICT_POS) {
+                continue;
+            }
+            // Word (unigram) probability
+            int word1Probability = NOT_A_PROBABILITY;
+            const int codePointCount = getCodePointsAndProbabilityAndReturnCodePointCount(
+                    word1TerminalPtNodePos, MAX_WORD_LENGTH, bigramWord1CodePoints,
+                    &word1Probability);
+            std::vector<int> word1(bigramWord1CodePoints,
+                    bigramWord1CodePoints + codePointCount);
+            const HistoricalInfo *const historicalInfo = bigramEntry.getHistoricalInfo();
+            const int probability = bigramEntry.hasHistoricalInfo() ?
+                    ForgettingCurveUtils::decodeProbability(bigramEntry.getHistoricalInfo()) :
+                    bigramEntry.getProbability();
+            bigrams.push_back(WordProperty::BigramProperty(&word1, probability,
+                    historicalInfo->getTimeStamp(), historicalInfo->getLevel(),
+                    historicalInfo->getCount()));
+        }
+    }
     // Fetch shortcut information.
-    std::vector<std::vector<int> > shortcutTargets;
-    std::vector<int> shortcutProbabilities;
+    std::vector<WordProperty::ShortcutProperty> shortcuts;
     int shortcutPos = getShortcutPositionOfPtNode(ptNodePos);
     if (shortcutPos != NOT_A_DICT_POS) {
         int shortcutTarget[MAX_WORD_LENGTH];
@@ -347,15 +382,20 @@
             shortcutDictContent->getShortcutEntryAndAdvancePosition(MAX_WORD_LENGTH, shortcutTarget,
                     &shortcutTargetLength, &shortcutProbability, &hasNext, &shortcutPos);
             std::vector<int> target(shortcutTarget, shortcutTarget + shortcutTargetLength);
-            shortcutTargets.push_back(target);
-            shortcutProbabilities.push_back(shortcutProbability);
+            shortcuts.push_back(WordProperty::ShortcutProperty(&target, shortcutProbability));
         }
     }
     return WordProperty(&codePointVector, ptNodeParams.isNotAWord(),
             ptNodeParams.isBlacklisted(), ptNodeParams.hasBigrams(),
             ptNodeParams.hasShortcutTargets(), ptNodeParams.getProbability(),
             historicalInfo->getTimeStamp(), historicalInfo->getLevel(),
-            historicalInfo->getCount(), &shortcutTargets, &shortcutProbabilities);
+            historicalInfo->getCount(), &bigrams, &shortcuts);
+}
+
+int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token,
+        int *const outCodePoints) {
+    // TODO: Implement.
+    return 0;
 }
 
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
index a43bd0e..1bcd4ce 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.h
@@ -109,6 +109,8 @@
     const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const;
 
+    int getNextWordAndNextToken(const int token, int *const outCodePoints);
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Ver4PatriciaTriePolicy);
 
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
similarity index 99%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
index 7b7f175..5f301a8 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
@@ -31,7 +31,7 @@
 import java.util.Locale;
 
 @MediumTest
-public class KeySpecParserSplitTests extends InstrumentationTestCase {
+public class MoreKeySpecSplitTests extends InstrumentationTestCase {
     private static final Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
@@ -93,7 +93,7 @@
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
         final String resolvedActual = mTextsSet.resolveTextReference(value);
-        final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual);
+        final String[] actual = MoreKeySpec.splitKeySpecs(resolvedActual);
         final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
         assertArrayEquals(message, expected, actual);
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
similarity index 95%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
index 0388435..538ba2c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
@@ -32,7 +32,7 @@
 import java.util.Locale;
 
 @SmallTest
-public class KeySpecParserTests extends AndroidTestCase {
+public class MoreKeySpecTests extends AndroidTestCase {
     private final static Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@@ -71,8 +71,9 @@
         mSettingsIconId = KeySpecParser.getIconId(ICON_SETTINGS);
     }
 
-    private void assertParser(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
+    private void assertParser(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
         final String labelResolved = mTextsSet.resolveTextReference(moreKeySpec);
         final MoreKeySpec spec = new MoreKeySpec(labelResolved, false /* needsToUpperCase */,
                 Locale.US, mCodesSet);
@@ -86,8 +87,9 @@
                 Constants.printableCode(spec.mCode));
     }
 
-    private void assertParserError(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
+    private void assertParserError(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
         try {
             assertParser(message, moreKeySpec, expectedLabel, expectedOutputText, expectedIcon,
                     expectedCode);
@@ -339,7 +341,8 @@
                 null, null, mSettingsIconId, mCodeSettings);
     }
 
-    private static void assertArrayEquals(String message, Object[] expected, Object[] actual) {
+    private static void assertArrayEquals(final String message, final Object[] expected,
+            final Object[] actual) {
         if (expected == actual) {
             return;
         }
@@ -357,10 +360,9 @@
         }
     }
 
-    private static void assertInsertAdditionalMoreKeys(String message, String[] moreKeys,
-            String[] additionalMoreKeys, String[] expected) {
-        final String[] actual =
-                KeySpecParser.insertAdditionalMoreKeys( moreKeys, additionalMoreKeys);
+    private static void assertInsertAdditionalMoreKeys(final String message,
+            final String[] moreKeys, final String[] additionalMoreKeys, final String[] expected) {
+        final String[] actual = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         assertArrayEquals(message, expected, actual);
     }
 
@@ -584,10 +586,10 @@
     private static final String AUTO_COLUMN_ORDER = "!autoColumnOrder!";
     private static final String FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
 
-    private static void assertGetBooleanValue(String message, String key, String[] moreKeys,
-            String[] expected, boolean expectedValue) {
+    private static void assertGetBooleanValue(final String message, final String key,
+            final String[] moreKeys, final String[] expected, final boolean expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final boolean actualValue = KeySpecParser.getBooleanValue(actual, key);
+        final boolean actualValue = MoreKeySpec.getBooleanValue(actual, key);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
@@ -622,10 +624,11 @@
                     "a", null, "b", NEEDS_DIVIDER, "!HASLABEL!", "d" }, true);
     }
 
-    private static void assertGetIntValue(String message, String key, int defaultValue,
-            String[] moreKeys, String[] expected, int expectedValue) {
+    private static void assertGetIntValue(final String message, final String key,
+            final int defaultValue, final String[] moreKeys, final String[] expected,
+            final int expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final int actualValue = KeySpecParser.getIntValue(actual, key, defaultValue);
+        final int actualValue = MoreKeySpec.getIntValue(actual, key, defaultValue);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index 844fcbb..e39b46f 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -871,14 +871,15 @@
         }
     }
 
-    public void testGetUnigramProperties() {
-        testGetUnigramProperties(FormatSpec.VERSION4);
+    public void testGetWordProperties() {
+        testGetWordProperties(FormatSpec.VERSION4);
     }
 
-    private void testGetUnigramProperties(final int formatVersion) {
+    private void testGetWordProperties(final int formatVersion) {
         final long seed = System.currentTimeMillis();
         final Random random = new Random(seed);
-        final int ITERATION_COUNT = 1000;
+        final int UNIGRAM_COUNT = 1000;
+        final int BIGRAM_COUNT = 1000;
         final int codePointSetSize = 20;
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
 
@@ -892,10 +893,16 @@
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
 
-        final WordProperty invalidUnigramProperty = binaryDictionary.getWordProperty("dummyWord");
-        assertFalse(invalidUnigramProperty.isValid());
+        final WordProperty invalidWordProperty = binaryDictionary.getWordProperty("dummyWord");
+        assertFalse(invalidWordProperty.isValid());
 
-        for (int i = 0; i < ITERATION_COUNT; i++) {
+        final ArrayList<String> words = new ArrayList<String>();
+        final HashMap<String, Integer> wordProbabilities = new HashMap<String, Integer>();
+        final HashMap<String, HashSet<String>> bigrams = new HashMap<String, HashSet<String>>();
+        final HashMap<Pair<String, String>, Integer> bigramProbabilities =
+                new HashMap<Pair<String, String>, Integer>();
+
+        for (int i = 0; i < UNIGRAM_COUNT; i++) {
             final String word = CodePointUtils.generateWord(random, codePointSet);
             final int unigramProbability = random.nextInt(0xFF);
             final boolean isNotAWord = random.nextBoolean();
@@ -904,6 +911,11 @@
             binaryDictionary.addUnigramWord(word, unigramProbability,
                     null /* shortcutTarget */, BinaryDictionary.NOT_A_PROBABILITY,
                     isNotAWord, isBlacklisted, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+            if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
+                binaryDictionary.flushWithGC();
+            }
+            words.add(word);
+            wordProbabilities.put(word, unigramProbability);
             final WordProperty unigramProperty = binaryDictionary.getWordProperty(word);
             assertEquals(word, unigramProperty.mCodePoints);
             assertTrue(unigramProperty.isValid());
@@ -911,9 +923,52 @@
             assertEquals(isBlacklisted, unigramProperty.mIsBlacklisted);
             assertEquals(false, unigramProperty.mHasBigrams);
             assertEquals(false, unigramProperty.mHasShortcuts);
-            assertEquals(unigramProbability, unigramProperty.mProbability);
+            assertEquals(unigramProbability, unigramProperty.mProbabilityInfo.mProbability);
             assertTrue(unigramProperty.mShortcutTargets.isEmpty());
         }
+
+        for (int i = 0; i < BIGRAM_COUNT; i++) {
+            final int word0Index = random.nextInt(wordProbabilities.size());
+            final int word1Index = random.nextInt(wordProbabilities.size());
+            if (word0Index == word1Index) {
+                continue;
+            }
+            final String word0 = words.get(word0Index);
+            final String word1 = words.get(word1Index);
+            final int bigramProbability = random.nextInt(0xF);
+            binaryDictionary.addBigramWords(word0, word1, bigramProbability,
+                    BinaryDictionary.NOT_A_VALID_TIMESTAMP);
+            if (binaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) {
+                binaryDictionary.flushWithGC();
+            }
+            if (!bigrams.containsKey(word0)) {
+                final HashSet<String> bigramWord1s = new HashSet<String>();
+                bigrams.put(word0, bigramWord1s);
+            }
+            bigrams.get(word0).add(word1);
+            bigramProbabilities.put(new Pair<String, String>(word0, word1), bigramProbability);
+        }
+
+        for (int i = 0; i < words.size(); i++) {
+            final String word0 = words.get(i);
+            if (!bigrams.containsKey(word0)) {
+                continue;
+            }
+            final HashSet<String> bigramWord1s = bigrams.get(word0);
+            final WordProperty unigramProperty = binaryDictionary.getWordProperty(word0);
+            assertEquals(bigramWord1s.size(), unigramProperty.mBigramTargets.size());
+            assertEquals(unigramProperty.mBigramTargets.size(),
+                    unigramProperty.mBigramProbabilityInfo.size());
+            for (int j = 0; j < unigramProperty.mBigramTargets.size(); j++) {
+                final String word1 = unigramProperty.mBigramTargets.get(j).mWord;
+                assertTrue(bigramWord1s.contains(word1));
+                final int probability = unigramProperty.mBigramTargets.get(j).mFrequency;
+                assertEquals((int)bigramProbabilities.get(new Pair<String, String>(word0, word1)),
+                        probability);
+                assertEquals(unigramProperty.mBigramProbabilityInfo.get(j).mProbability,
+                        probability);
+            }
+        }
     }
 
     public void testAddShortcuts() {
@@ -936,28 +991,28 @@
         binaryDictionary.addUnigramWord("aaa", unigramProbability, "zzz",
                 shortcutProbability, false /* isNotAWord */, false /* isBlacklisted */,
                 0 /* timestamp */);
-        WordProperty unigramProperty = binaryDictionary.getWordProperty("aaa");
-        assertEquals(1, unigramProperty.mShortcutTargets.size());
-        assertEquals("zzz", unigramProperty.mShortcutTargets.get(0).mWord);
-        assertEquals(shortcutProbability, unigramProperty.mShortcutTargets.get(0).mFrequency);
+        WordProperty wordProperty = binaryDictionary.getWordProperty("aaa");
+        assertEquals(1, wordProperty.mShortcutTargets.size());
+        assertEquals("zzz", wordProperty.mShortcutTargets.get(0).mWord);
+        assertEquals(shortcutProbability, wordProperty.mShortcutTargets.get(0).mFrequency);
         final int updatedShortcutProbability = 2;
         binaryDictionary.addUnigramWord("aaa", unigramProbability, "zzz",
                 updatedShortcutProbability, false /* isNotAWord */, false /* isBlacklisted */,
                 0 /* timestamp */);
-        unigramProperty = binaryDictionary.getWordProperty("aaa");
-        assertEquals(1, unigramProperty.mShortcutTargets.size());
-        assertEquals("zzz", unigramProperty.mShortcutTargets.get(0).mWord);
+        wordProperty = binaryDictionary.getWordProperty("aaa");
+        assertEquals(1, wordProperty.mShortcutTargets.size());
+        assertEquals("zzz", wordProperty.mShortcutTargets.get(0).mWord);
         assertEquals(updatedShortcutProbability,
-                unigramProperty.mShortcutTargets.get(0).mFrequency);
+                wordProperty.mShortcutTargets.get(0).mFrequency);
         binaryDictionary.addUnigramWord("aaa", unigramProbability, "yyy",
                 shortcutProbability, false /* isNotAWord */, false /* isBlacklisted */,
                 0 /* timestamp */);
         final HashMap<String, Integer> shortcutTargets = new HashMap<String, Integer>();
         shortcutTargets.put("zzz", updatedShortcutProbability);
         shortcutTargets.put("yyy", shortcutProbability);
-        unigramProperty = binaryDictionary.getWordProperty("aaa");
-        assertEquals(2, unigramProperty.mShortcutTargets.size());
-        for (WeightedString shortcutTarget : unigramProperty.mShortcutTargets) {
+        wordProperty = binaryDictionary.getWordProperty("aaa");
+        assertEquals(2, wordProperty.mShortcutTargets.size());
+        for (WeightedString shortcutTarget : wordProperty.mShortcutTargets) {
             assertTrue(shortcutTargets.containsKey(shortcutTarget.mWord));
             assertEquals((int)shortcutTargets.get(shortcutTarget.mWord), shortcutTarget.mFrequency);
             shortcutTargets.remove(shortcutTarget.mWord);
@@ -965,9 +1020,9 @@
         shortcutTargets.put("zzz", updatedShortcutProbability);
         shortcutTargets.put("yyy", shortcutProbability);
         binaryDictionary.flushWithGC();
-        unigramProperty = binaryDictionary.getWordProperty("aaa");
-        assertEquals(2, unigramProperty.mShortcutTargets.size());
-        for (WeightedString shortcutTarget : unigramProperty.mShortcutTargets) {
+        wordProperty = binaryDictionary.getWordProperty("aaa");
+        assertEquals(2, wordProperty.mShortcutTargets.size());
+        for (WeightedString shortcutTarget : wordProperty.mShortcutTargets) {
             assertTrue(shortcutTargets.containsKey(shortcutTarget.mWord));
             assertEquals((int)shortcutTargets.get(shortcutTarget.mWord), shortcutTarget.mFrequency);
             shortcutTargets.remove(shortcutTarget.mWord);
@@ -1034,14 +1089,15 @@
         }
 
         for (final String word : words) {
-            final WordProperty unigramProperty = binaryDictionary.getWordProperty(word);
-            assertEquals((int)unigramProbabilities.get(word), unigramProperty.mProbability);
+            final WordProperty wordProperty = binaryDictionary.getWordProperty(word);
+            assertEquals((int)unigramProbabilities.get(word),
+                    wordProperty.mProbabilityInfo.mProbability);
             if (!shortcutTargets.containsKey(word)) {
                 // The word does not have shortcut targets.
                 continue;
             }
-            assertEquals(shortcutTargets.get(word).size(), unigramProperty.mShortcutTargets.size());
-            for (final WeightedString shortcutTarget : unigramProperty.mShortcutTargets) {
+            assertEquals(shortcutTargets.get(word).size(), wordProperty.mShortcutTargets.size());
+            for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) {
                 final String targetCodePonts = shortcutTarget.mWord;
                 assertEquals((int)shortcutTargets.get(word).get(targetCodePonts),
                         shortcutTarget.mFrequency);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 8a1ac52..715db2f 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -108,6 +108,19 @@
         }
     }
 
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        BinaryDictionary.setCurrentTimeForTest(0);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        // Quit test mode.
+        BinaryDictionary.setCurrentTimeForTest(-1);
+    }
+
     private void generateWords(final int number, final Random random) {
         final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE,
                 random);
diff --git a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
index 9252992..db1dde3 100644
--- a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+++ b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -47,7 +48,7 @@
  */
 public final class KeyboardTextsSet {
     public static final String PREFIX_TEXT = "!text/";
-    private static final char BACKSLASH = '\\';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
 
     // Language to texts map.