Merge "Introduce a delay for key-press feedback on the Emoji palette"
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/suggestion_word.xml b/java/res/layout/suggestion_word.xml
deleted file mode 100644
index 47d2bd8..0000000
--- a/java/res/layout/suggestion_word.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2011, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-**     http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
--->
-
-<!-- Provide a haptic feedback by ourselves based on the keyboard settings.
-     We just need to ignore the system's haptic feedback settings. -->
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:minWidth="@dimen/config_suggestion_min_width"
-    android:textSize="@dimen/config_suggestion_text_size"
-    android:gravity="center"
-    android:paddingLeft="@dimen/config_suggestion_text_horizontal_padding"
-    android:paddingTop="0dp"
-    android:paddingRight="@dimen/config_suggestion_text_horizontal_padding"
-    android:paddingBottom="0dp"
-    android:hapticFeedbackEnabled="false"
-    android:focusable="false"
-    android:clickable="false"
-    android:singleLine="true"
-    android:ellipsize="none"
-    style="?attr/suggestionWordStyle" />
diff --git a/java/res/layout/suggestions_strip.xml b/java/res/layout/suggestions_strip.xml
index 85d60e6..9ffd231 100644
--- a/java/res/layout/suggestions_strip.xml
+++ b/java/res/layout/suggestions_strip.xml
@@ -31,13 +31,31 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:visibility="invisible">
-        <include
-            layout="@layout/suggestion_word"
-            android:id="@+id/word_to_save" />
+        <TextView
+            android:id="@+id/word_to_save"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            style="?attr/suggestionWordStyle" />
         <include
             layout="@layout/suggestion_divider" />
-        <include
-            layout="@layout/suggestion_word"
-            android:id="@+id/hint_add_to_dictionary" />
+        <TextView
+            android:id="@+id/hint_add_to_dictionary"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            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/layout/suggestion_info.xml b/java/res/values/strings-config-important-notice.xml
similarity index 61%
rename from java/res/layout/suggestion_info.xml
rename to java/res/values/strings-config-important-notice.xml
index 0aa2600..da206a3 100644
--- a/java/res/layout/suggestion_info.xml
+++ b/java/res/values/strings-config-important-notice.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2011, The Android Open Source Project
+** Copyright 2014, The Android Open Source Project
 **
 ** Licensed under the Apache License, Version 2.0 (the "License");
 ** you may not use this file except in compliance with the License.
@@ -18,10 +18,10 @@
 */
 -->
 
-<TextView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:textSize="6dp"
-    android:textColor="@android:color/white"
-    style="?attr/suggestionWordStyle" />
+<resources>
+    <integer name="config_important_notice_version">0</integer>
+    <!-- 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/res/values/themes-common.xml b/java/res/values/themes-common.xml
index 7c9b51c..3bfd73a 100644
--- a/java/res/values/themes-common.xml
+++ b/java/res/values/themes-common.xml
@@ -127,7 +127,22 @@
         <item name="maxMoreSuggestionsRow">@integer/config_max_more_suggestions_row</item>
         <item name="minMoreSuggestionsWidth">@fraction/config_min_more_suggestions_width</item>
     </style>
-    <style name="SuggestionWord" />
+    <style name="SuggestionWord">
+        <item name="android:minWidth">@dimen/config_suggestion_min_width</item>
+        <item name="android:textSize">@dimen/config_suggestion_text_size</item>
+        <item name="android:gravity">center</item>
+        <item name="android:paddingLeft">@dimen/config_suggestion_text_horizontal_padding</item>
+        <item name="android:paddingTop">0dp</item>
+        <item name="android:paddingRight">@dimen/config_suggestion_text_horizontal_padding</item>
+        <item name="android:paddingBottom">0dp</item>
+        <!-- Provide a haptic feedback by ourselves based on the keyboard settings.
+             We just need to ignore the system's haptic feedback settings. -->
+        <item name="android:hapticFeedbackEnabled">false</item>
+        <item name="android:focusable">false</item>
+        <item name="android:clickable">false</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:ellipsize">none</item>
+    </style>
     <style name="MoreKeysKeyboardAnimation">
         <item name="android:windowEnterAnimation">@anim/more_keys_keyboard_fadein</item>
         <item name="android:windowExitAnimation">@anim/more_keys_keyboard_fadeout</item>
diff --git a/java/res/values/themes-gb.xml b/java/res/values/themes-gb.xml
index 52ecafd..a460d4f 100644
--- a/java/res/values/themes-gb.xml
+++ b/java/res/values/themes-gb.xml
@@ -140,7 +140,11 @@
         <item name="colorSuggested">@color/highlight_color_gb</item>
         <item name="alphaObsoleted">50%</item>
     </style>
-    <style name="SuggestionWord.GB">
+    <style
+        name="SuggestionWord.GB"
+        parent="SuggestionWord"
+    >
         <item name="android:background">@drawable/btn_suggestion_gb</item>
+        <item name="android:textColor">@color/highlight_color_gb</item>
     </style>
 </resources>
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index 125b640..caea921 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -119,7 +119,11 @@
         <item name="colorSuggested">@color/suggested_word_color_ics</item>
         <item name="alphaObsoleted">70%</item>
     </style>
-    <style name="SuggestionWord.ICS">
+    <style
+        name="SuggestionWord.ICS"
+        parent="SuggestionWord"
+    >
         <item name="android:background">@drawable/btn_suggestion_ics</item>
+        <item name="android:textColor">@color/highlight_color_ics</item>
     </style>
 </resources>
diff --git a/java/res/values/themes-klp.xml b/java/res/values/themes-klp.xml
index f6c0767..0599fb6 100644
--- a/java/res/values/themes-klp.xml
+++ b/java/res/values/themes-klp.xml
@@ -119,7 +119,11 @@
         <item name="colorSuggested">@color/suggested_word_color_klp</item>
         <item name="alphaObsoleted">70%</item>
     </style>
-    <style name="SuggestionWord.KLP">
+    <style
+        name="SuggestionWord.KLP"
+        parent="SuggestionWord"
+    >
         <item name="android:background">@drawable/btn_suggestion_klp</item>
+        <item name="android:textColor">@color/highlight_color_klp</item>
     </style>
 </resources>
diff --git a/java/res/xml/keys_comma_period_symbols.xml b/java/res/xml/keys_comma_period_symbols.xml
index cbdbe2e..880fe40 100644
--- a/java/res/xml/keys_comma_period_symbols.xml
+++ b/java/res/xml/keys_comma_period_symbols.xml
@@ -22,7 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <Key
-        latin:keyLabel="," />
+        latin:keyLabel="!text/keylabel_for_comma"
+        latin:moreKeys="!text/more_keys_for_comma" />
     <!-- U+2026: "…" HORIZONTAL ELLIPSIS -->
     <Key
         latin:keyLabel="."
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index c8c4d30..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];
@@ -331,16 +331,16 @@
             // code point nor as a surrogate pair.
             mLabel = new StringBuilder().appendCodePoint(code).toString();
         } else {
-            mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+            mLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr,
                     R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale);
         }
         if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) {
             mHintLabel = null;
         } else {
-            mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+            mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr,
                     R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale);
         }
-        String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
+        String outputText = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale);
         // Choose the first letter of the label as primary code if not specified.
         if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
@@ -367,9 +367,9 @@
                 mCode = CODE_OUTPUT_TEXT;
             }
         } else {
-            mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
+            mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
         }
-        final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale(
+        final int altCode = StringUtils.toUpperCaseOfCodeForLocale(
                 KeySpecParser.parseCode(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
                 needsToUpperCase, locale);
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/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index accfaed..c51941d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -19,114 +19,47 @@
 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.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 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;
-
-    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
-
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final char BACKSLASH = '\\';
-    private static final char VERTICAL_BAR = '|';
-    private static final String PREFIX_TEXT = "!text/";
-    static final String PREFIX_ICON = "!icon/";
-    private static final String PREFIX_CODE = "!code/";
+    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(PREFIX_ICON);
-    }
-
-    private static boolean hasCode(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
-                PREFIX_CODE, end + 1)) {
+    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;
         }
         return false;
@@ -151,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++;
@@ -172,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)}.
@@ -237,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);
@@ -248,8 +181,8 @@
     public static int parseCode(final String text, final KeyboardCodesSet codesSet,
             final int defCode) {
         if (text == null) return defCode;
-        if (text.startsWith(PREFIX_CODE)) {
-            return codesSet.getCode(text.substring(PREFIX_CODE.length()));
+        if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
+            return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
         } else if (text.startsWith(PREFIX_HEX)) {
             return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
         } else {
@@ -257,245 +190,22 @@
         }
     }
 
-    public static int getIconId(final String moreKeySpec) {
-        if (moreKeySpec != null && hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length());
-            final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
-                    : moreKeySpec.substring(PREFIX_ICON.length(), end);
+    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)
+                    ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
+                    : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) {
-        if (array == null) {
-            throw new NullPointerException();
-        }
-        if (start < 0 || start > end || end > array.length) {
-            throw new IllegalArgumentException();
-        }
-
-        final ArrayList<T> list = CollectionUtils.newArrayList(end - start);
-        for (int i = start; i < end; i++) {
-            list.add(array[i]);
-        }
-        return list;
-    }
-
-    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 = 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 = 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 = 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 = 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 String resolveTextReference(final String rawText,
-            final KeyboardTextsSet textsSet) {
-        if (TextUtils.isEmpty(rawText)) {
-            return null;
-        }
-        int level = 0;
-        String text = rawText;
-        StringBuilder sb;
-        do {
-            level++;
-            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
-                throw new RuntimeException("too many @string/resource indirection: " + text);
-            }
-
-            final int prefixLen = PREFIX_TEXT.length();
-            final int size = text.length();
-            if (size < prefixLen) {
-                return TextUtils.isEmpty(text) ? null : text;
-            }
-
-            sb = null;
-            for (int pos = 0; pos < size; pos++) {
-                final char c = text.charAt(pos);
-                if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) {
-                    if (sb == null) {
-                        sb = new StringBuilder(text.substring(0, pos));
-                    }
-                    final int end = searchTextNameEnd(text, pos + prefixLen);
-                    final String name = text.substring(pos + prefixLen, end);
-                    sb.append(textsSet.getText(name));
-                    pos = end - 1;
-                } else if (c == BACKSLASH) {
-                    if (sb != null) {
-                        // Append both escape character and escaped character.
-                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
-                    }
-                    pos++;
-                } else if (sb != null) {
-                    sb.append(c);
-                }
-            }
-
-            if (sb != null) {
-                text = sb.toString();
-            }
-        } while (sb != null);
-        return TextUtils.isEmpty(text) ? null : text;
-    }
-
-    private static int searchTextNameEnd(final String text, final int start) {
-        final int size = text.length();
-        for (int pos = start; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            // Label name should be consisted of [a-zA-Z_0-9].
-            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
-                continue;
-            }
-            return pos;
-        }
-        return size;
-    }
-
-    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;
-    }
-
-    public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase,
-            final Locale locale) {
-        if (!Constants.isLetterCode(code) || !needsToUpperCase) return code;
-        final String text = StringUtils.newSingleCodePointString(code);
-        final String casedText = KeySpecParser.toUpperCaseOfStringForLocale(
-                text, needsToUpperCase, locale);
-        return StringUtils.codePointCount(casedText) == 1
-                ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
-    }
-
-    public static String toUpperCaseOfStringForLocale(final String text,
-            final boolean needsToUpperCase, final Locale locale) {
-        if (text == null || !needsToUpperCase) return text;
-        return text.toUpperCase(locale);
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index e6a6743..7941ddd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -32,15 +32,15 @@
 
     protected String parseString(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
-            return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
+            return mTextsSet.resolveTextReference(a.getString(index));
         }
         return null;
     }
 
     protected String[] parseStringArray(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
-            final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
-            return KeySpecParser.splitKeySpecs(text);
+            final String text = mTextsSet.resolveTextReference(a.getString(index));
+            return MoreKeySpec.splitKeySpecs(text);
         }
         return null;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 78809d5..aeb4db5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -22,6 +22,8 @@
 import java.util.HashMap;
 
 public final class KeyboardCodesSet {
+    public static final String PREFIX_CODE = "!code/";
+
     private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap();
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 0ee935f..da8bf7d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -30,6 +30,7 @@
 public final class KeyboardIconsSet {
     private static final String TAG = KeyboardIconsSet.class.getSimpleName();
 
+    public static final String PREFIX_ICON = "!icon/";
     public static final int ICON_UNDEFINED = 0;
     private static final int ATTR_UNDEFINED = 0;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 9f33fcc..f8c0988 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+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;
@@ -45,6 +47,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+
     // Language to texts map.
     private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
@@ -87,6 +93,67 @@
         return (text == null) ? LANGUAGE_DEFAULT[id] : text;
     }
 
+    private static int searchTextNameEnd(final String text, final int start) {
+        final int size = text.length();
+        for (int pos = start; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // Label name should be consisted of [a-zA-Z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
+
+    public String resolveTextReference(final String rawText) {
+        if (TextUtils.isEmpty(rawText)) {
+            return null;
+        }
+        int level = 0;
+        String text = rawText;
+        StringBuilder sb;
+        do {
+            level++;
+            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
+                throw new RuntimeException("too many @string/resource indirection: " + text);
+            }
+
+            final int prefixLen = PREFIX_TEXT.length();
+            final int size = text.length();
+            if (size < prefixLen) {
+                return TextUtils.isEmpty(text) ? null : text;
+            }
+
+            sb = null;
+            for (int pos = 0; pos < size; pos++) {
+                final char c = text.charAt(pos);
+                if (text.startsWith(PREFIX_TEXT, pos)) {
+                    if (sb == null) {
+                        sb = new StringBuilder(text.substring(0, pos));
+                    }
+                    final int end = searchTextNameEnd(text, pos + prefixLen);
+                    final String name = text.substring(pos + prefixLen, end);
+                    sb.append(getText(name));
+                    pos = end - 1;
+                } else if (c == BACKSLASH) {
+                    if (sb != null) {
+                        // Append both escape character and escaped character.
+                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
+                    }
+                    pos++;
+                } else if (sb != null) {
+                    sb.append(c);
+                }
+            }
+
+            if (sb != null) {
+                text = sb.toString();
+            }
+        } while (sb != null);
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
     // These texts' name should be aligned with the @string/<name> in
     // values*/strings-action-keys.xml.
     private static final String[] RESOURCE_NAMES = {
@@ -1512,7 +1579,8 @@
         /* 104 */ "\u061F",
         /* 105 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB",
         /* 106 */ null,
-        /* 107 */ "\u061F",
+        // U+064B: "ً" ARABIC FATHATAN
+        /* 107 */ "\u064B",
         /* 108 */ "!text/more_keys_for_arabic_diacritics",
         /* 109 */ null,
         /* 110 */ "\u064B",
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index 110936f..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;
@@ -31,9 +46,9 @@
 
     public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale,
             final KeyboardCodesSet codesSet) {
-        mLabel = KeySpecParser.toUpperCaseOfStringForLocale(
+        mLabel = StringUtils.toUpperCaseOfStringForLocale(
                 KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
-        final int code = KeySpecParser.toUpperCaseOfCodeForLocale(
+        final int code = StringUtils.toUpperCaseOfCodeForLocale(
                 KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale);
         if (code == Constants.CODE_UNSPECIFIED) {
             // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
@@ -42,7 +57,7 @@
             mOutputText = mLabel;
         } else {
             mCode = code;
-            mOutputText = KeySpecParser.toUpperCaseOfStringForLocale(
+            mOutputText = StringUtils.toUpperCaseOfStringForLocale(
                     KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
         }
         mIconId = KeySpecParser.getIconId(moreKeySpec);
@@ -74,7 +89,7 @@
     @Override
     public String toString() {
         final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
-                : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
+                : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
         final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
                 : Constants.printableCode(mCode));
         if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
@@ -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 e66cfca..6e0cdf2 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -27,7 +27,7 @@
 import com.android.inputmethod.latin.utils.JniUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
 import com.android.inputmethod.latin.utils.StringUtils;
-import com.android.inputmethod.latin.utils.UnigramProperty;
+import com.android.inputmethod.latin.utils.WordProperty;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -61,18 +61,19 @@
 
     public static final int NOT_A_VALID_TIMESTAMP = -1;
 
-    // Format to get unigram flags from native side via getUnigramPropertyNative().
-    private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT = 4;
-    private static final int FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
-    private static final int FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX = 1;
-    private static final int FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX = 2;
-    private static final int FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX = 3;
+    // Format to get unigram flags from native side via getWordPropertyNative().
+    private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 4;
+    private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0;
+    private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1;
+    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 getUnigramPropertyNative().
-    private static final int FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT = 3;
-    private static final int FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX = 0;
-    private static final int FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX = 1;
-    private static final int FORMAT_UNIGRAM_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;
@@ -143,10 +144,10 @@
     private static native int getFormatVersionNative(long dict);
     private static native int getProbabilityNative(long dict, int[] word);
     private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
-    private static native void getUnigramPropertyNative(long dict, int[] word,
-            int[] outCodePoints, boolean[] outFlags, int[] outProbability,
-            int[] outHistoricalInfo, ArrayList<int[]> outShortcutTargets,
-            ArrayList<Integer> outShortcutProbabilities);
+    private static native void getWordPropertyNative(long dict, int[] word,
+            int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo,
+            ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo,
+            ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities);
     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,
@@ -306,29 +307,29 @@
     }
 
     @UsedForTesting
-    public UnigramProperty getUnigramProperty(final String word) {
+    public WordProperty getWordProperty(final String word) {
         if (TextUtils.isEmpty(word)) {
             return null;
         }
         final int[] codePoints = StringUtils.toCodePointArray(word);
         final int[] outCodePoints = new int[MAX_WORD_LENGTH];
-        final boolean[] outFlags = new boolean[FORMAT_UNIGRAM_PROPERTY_OUTPUT_FLAG_COUNT];
-        final int[] outProbability = new int[1];
-        final int[] outHistoricalInfo =
-                new int[FORMAT_UNIGRAM_PROPERTY_OUTPUT_HISTORICAL_INFO_COUNT];
+        final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_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();
-        getUnigramPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbability,
-                outHistoricalInfo, outShortcutTargets, outShortcutProbabilities);
-        return new UnigramProperty(codePoints,
-                outFlags[FORMAT_UNIGRAM_PROPERTY_IS_NOT_A_WORD_INDEX],
-                outFlags[FORMAT_UNIGRAM_PROPERTY_IS_BLACKLISTED_INDEX],
-                outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_BIGRAMS_INDEX],
-                outFlags[FORMAT_UNIGRAM_PROPERTY_HAS_SHORTCUTS_INDEX], outProbability[0],
-                outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_TIMESTAMP_INDEX],
-                outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_LEVEL_INDEX],
-                outHistoricalInfo[FORMAT_UNIGRAM_PROPERTY_COUNT_INDEX],
-                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], outProbabilityInfo,
+                outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+                outShortcutProbabilities);
     }
 
     // Add a unigram entry to binary dictionary with unigram attributes in native code.
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/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
index 8a8a45f..8b02984 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
@@ -505,6 +505,14 @@
         }
     }
 
+    @UsedForTesting
+    public void clearUserHistoryDictionary() {
+        if (mUserHistoryDictionary == null) {
+            return;
+        }
+        mUserHistoryDictionary.clearAndFlushDictionary();
+    }
+
     // This method gets called only when the IME receives a notification to remove the
     // personalization dictionary.
     public void clearPersonalizationDictionary() {
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 8e1675b..4dee84a 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -267,9 +267,9 @@
 
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         return attributeMap;
     }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index e8ea5e3..208cf22 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -42,7 +42,6 @@
 import android.text.InputType;
 import android.text.TextUtils;
 import android.util.Log;
-import android.util.Pair;
 import android.util.PrintWriterPrinter;
 import android.util.Printer;
 import android.view.KeyEvent;
@@ -154,8 +153,7 @@
         private static final int ARG1_NOT_GESTURE_INPUT = 0;
         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
         private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2;
-        private static final int ARG2_WITHOUT_TYPED_WORD = 0;
-        private static final int ARG2_WITH_TYPED_WORD = 1;
+        private static final int ARG2_UNUSED = 0;
 
         private int mDelayUpdateSuggestions;
         private int mDelayUpdateShiftState;
@@ -190,16 +188,8 @@
                 break;
             case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
                 if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) {
-                    if (msg.arg2 == ARG2_WITH_TYPED_WORD) {
-                        final Pair<SuggestedWords, String> p =
-                                (Pair<SuggestedWords, String>) msg.obj;
-                        // [IL]: this is the only place where the second arg is not
-                        // suggestedWords.mTypedWord.
-                        latinIme.showSuggestionStrip(p.first, p.second);
-                    } else {
-                        final SuggestedWords suggestedWords = (SuggestedWords) msg.obj;
-                        latinIme.showSuggestionStrip(suggestedWords, suggestedWords.mTypedWord);
-                    }
+                    final SuggestedWords suggestedWords = (SuggestedWords) msg.obj;
+                    latinIme.showSuggestionStrip(suggestedWords);
                 } else {
                     latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
                             msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
@@ -292,22 +282,13 @@
                     ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT
                     : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT;
             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1,
-                    ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
+                    ARG2_UNUSED, suggestedWords).sendToTarget();
         }
 
         public void showSuggestionStrip(final SuggestedWords suggestedWords) {
             removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
             obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP,
-                    ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget();
-        }
-
-        // TODO: Remove this method.
-        public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
-                final String typedWord) {
-            removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
-            obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT,
-                    ARG2_WITH_TYPED_WORD,
-                    new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget();
+                    ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget();
         }
 
         public void onEndBatchInput(final SuggestedWords suggestedWords) {
@@ -1176,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();
@@ -1273,7 +1261,7 @@
     // This method must run on the UI Thread.
     private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
             final boolean dismissGestureFloatingPreviewText) {
-        showSuggestionStrip(suggestedWords, suggestedWords.mTypedWord);
+        showSuggestionStrip(suggestedWords);
         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
         mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
         if (dismissGestureFloatingPreviewText) {
@@ -1409,8 +1397,7 @@
     }
 
     // TODO[IL]: Define a clean interface for this
-    public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords,
-            final String typedWord) {
+    public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) {
         final SuggestedWords suggestedWords =
                 sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords;
         final String autoCorrection;
@@ -1419,7 +1406,7 @@
         } else {
             // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
             // because it may differ from mWordComposer.mTypedWord.
-            autoCorrection = typedWord;
+            autoCorrection = sourceSuggestedWords.mTypedWord;
         }
         if (SuggestedWords.EMPTY != suggestedWords) {
             mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
@@ -1427,7 +1414,8 @@
         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, typedWord);
+        AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords,
+                sourceSuggestedWords.mTypedWord);
     }
 
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
@@ -1517,8 +1505,7 @@
         }
         if (showingAddToDictionaryHint
                 && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) {
-            mSuggestionStripView.showAddToDictionaryHint(
-                    suggestion, currentSettings.mHintToSaveText);
+            mSuggestionStripView.showAddToDictionaryHint(suggestion);
         } else {
             // If we're not showing the "Touch again to save", then update the suggestion strip.
             mHandler.postUpdateSuggestionStrip();
@@ -1538,18 +1525,6 @@
         }
     }
 
-    public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
-            final SuggestedWords suggestedWords, final String typedWord) {
-        // Note that it's very important here that suggestedWords.mWillAutoCorrect is false.
-        // We never want to auto-correct on a resumed suggestion. Please refer to the three places
-        // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected.
-        // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
-        // the text to adapt it.
-        // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
-        mInputLogic.mIsAutoCorrectionIndicatorOn = false;
-        mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
-    }
-
     // TODO: Make this private
     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
     @UsedForTesting
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 982a97a..dbaf822 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -68,6 +68,21 @@
             final boolean isObsoleteSuggestions,
             final boolean isPrediction,
             final int sequenceNumber) {
+        this(suggestedWordInfoList,
+                suggestedWordInfoList.isEmpty() ? null
+                        : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord,
+                typedWordValid, willAutoCorrect, isPunctuationSuggestions,
+                isObsoleteSuggestions, isPrediction, sequenceNumber);
+    }
+
+    public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
+            final String typedWord,
+            final boolean typedWordValid,
+            final boolean willAutoCorrect,
+            final boolean isPunctuationSuggestions,
+            final boolean isObsoleteSuggestions,
+            final boolean isPrediction,
+            final int sequenceNumber) {
         mSuggestedWordInfoList = suggestedWordInfoList;
         mTypedWordValid = typedWordValid;
         mWillAutoCorrect = willAutoCorrect;
@@ -75,7 +90,7 @@
         mIsObsoleteSuggestions = isObsoleteSuggestions;
         mIsPrediction = isPrediction;
         mSequenceNumber = sequenceNumber;
-        mTypedWord = suggestedWordInfoList.isEmpty() ? null : getWord(INDEX_OF_TYPED_WORD);
+        mTypedWord = typedWord;
     }
 
     public boolean isEmpty() {
@@ -279,17 +294,21 @@
     // words from the member ArrayList as some other parties may expect the object to never change.
     public SuggestedWords getSuggestedWordsExcludingTypedWord() {
         final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList();
+        String typedWord = null;
         for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
             final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
             if (SuggestedWordInfo.KIND_TYPED != info.mKind) {
                 newSuggestions.add(info);
+            } else {
+                assert(null == typedWord);
+                typedWord = info.mWord;
             }
         }
         // We should never autocorrect, so we say the typed word is valid. Also, in this case,
         // no auto-correction should take place hence willAutoCorrect = false.
-        return new SuggestedWords(newSuggestions, true /* typedWordValid */,
+        return new SuggestedWords(newSuggestions, typedWord, true /* typedWordValid */,
                 false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
-                mIsPrediction);
+                mIsPrediction, NOT_A_SEQUENCE_NUMBER);
     }
 
     // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index ce3ef53..1bc67b2 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -1120,7 +1120,7 @@
         final SuggestedWords suggestedWords = holder.get(null,
                 Constants.GET_SUGGESTED_WORDS_TIMEOUT);
         if (suggestedWords != null) {
-            mLatinIME.showSuggestionStrip(suggestedWords, suggestedWords.mTypedWord);
+            mLatinIME.showSuggestionStrip(suggestedWords);
         }
     }
 
@@ -1217,21 +1217,18 @@
                                 // Since there is only one word, willAutoCorrect is false.
                                 suggestedWords = suggestedWordsIncludingTypedWord;
                             }
-                            // We need to pass typedWord because mWordComposer.mTypedWord may
-                            // differ from typedWord.
-                            mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
-                                    suggestedWords, typedWord);
+                            mIsAutoCorrectionIndicatorOn = false;
+                            mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
                         }});
         } else {
             // We found suggestion spans in the word. We'll create the SuggestedWords out of
             // them, and make willAutoCorrect false.
-            final SuggestedWords suggestedWords = new SuggestedWords(suggestions,
+            final SuggestedWords suggestedWords = new SuggestedWords(suggestions, typedWord,
                     true /* typedWordValid */, false /* willAutoCorrect */,
                     false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */,
-                    false /* isPrediction */);
-            // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord.
-            mLatinIME.unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords,
-                    typedWord);
+                    false /* isPrediction */, SuggestedWords.NOT_A_SEQUENCE_NUMBER);
+            mIsAutoCorrectionIndicatorOn = false;
+            mLatinIME.mHandler.showSuggestionStrip(suggestedWords);
         }
     }
 
@@ -1624,8 +1621,7 @@
                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
                 final SuggestedWords suggestedWordsForLastWordOfPhraseGesture =
                         suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture();
-                mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture,
-                        suggestedWordsForLastWordOfPhraseGesture.mTypedWord);
+                mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture);
             }
             final String lastWord = batchInputText.substring(indexOfLastSpace);
             mWordComposer.setBatchInputWord(lastWord);
diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
index f8fa68f..1a91181 100644
--- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
@@ -48,7 +48,7 @@
           throw new UnsupportedFormatException("Unsupported version : " + version);
         }
         // TODO: Remove this field.
-        final int optionsFlags = HeaderReader.readOptionFlags(headerBuffer);
+        HeaderReader.readOptionFlags(headerBuffer);
         final int headerSize = HeaderReader.readHeaderSize(headerBuffer);
         if (headerSize < 0) {
             throw new UnsupportedFormatException("header size can't be negative.");
@@ -59,8 +59,8 @@
 
         final FileHeader header = new FileHeader(headerSize,
                 new FusionDictionary.DictionaryOptions(attributes),
-                new FormatOptions(version,
-                        0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG)));
+                new FormatOptions(version, FileHeader.ATTRIBUTE_VALUE_TRUE.equals(
+                        attributes.get(FileHeader.HAS_HISTORICAL_INFO_KEY))));
         return header;
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index 9a24c47..3174715 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -499,7 +499,6 @@
         final int nodeArrayOriginPos = dictDecoder.getPosition();
 
         do { // Scan the linked-list node.
-            final int nodeArrayHeadPos = dictDecoder.getPosition();
             final int count = dictDecoder.readPtNodeCount();
             int groupPos = dictDecoder.getPosition();
             for (int i = count; i > 0; --i) { // Scan the array of PtNode.
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
index bb40e0d..eff8fc3 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -756,14 +756,6 @@
     }
 
     /**
-     * Makes the 2-byte value for options flags. Unused at the moment, and always 0.
-     */
-    private static final int makeOptionsValue(final FormatOptions formatOptions) {
-        // TODO: why doesn't this handle CONTAINS_TIMESTAMP_FLAG?
-        return 0;
-    }
-
-    /**
      * Makes the flag value for a shortcut.
      *
      * @param more whether there are more attributes after this one.
@@ -949,7 +941,8 @@
         headerBuffer.write((byte) (0xFF & version));
 
         // Options flags
-        final int options = makeOptionsValue(formatOptions);
+        // TODO: Remove this field.
+        final int options = 0;
         headerBuffer.write((byte) (0xFF & (options >> 8)));
         headerBuffer.write((byte) (0xFF & options));
         final int headerSizeOffset = headerBuffer.size();
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 437fa94..74e3059 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -192,10 +192,6 @@
     static final int MINIMUM_SUPPORTED_VERSION = VERSION2;
     static final int MAXIMUM_SUPPORTED_VERSION = VERSION4;
 
-    // These options need to be the same numeric values as the one in the native reading code.
-    // TODO: Make the native reading code read this variable.
-    static final int CONTAINS_TIMESTAMP_FLAG = 0x10;
-
     // TODO: Make this value adaptative to content data, store it in the header, and
     // use it in the reading code.
     static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
@@ -249,26 +245,26 @@
     static final String TRIE_FILE_EXTENSION = ".trie";
     public static final String HEADER_FILE_EXTENSION = ".header";
     static final String FREQ_FILE_EXTENSION = ".freq";
-    static final String UNIGRAM_TIMESTAMP_FILE_EXTENSION = ".timestamp";
     // tat = Terminal Address Table
     static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat";
     static final String BIGRAM_FILE_EXTENSION = ".bigram";
     static final String SHORTCUT_FILE_EXTENSION = ".shortcut";
     static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup";
     static final String CONTENT_TABLE_FILE_SUFFIX = "_index";
+    static final int FLAGS_IN_FREQ_FILE_SIZE = 1;
     static final int FREQUENCY_AND_FLAGS_SIZE = 2;
     static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
     static final int UNIGRAM_TIMESTAMP_SIZE = 4;
+    static final int UNIGRAM_COUNTER_SIZE = 1;
+    static final int UNIGRAM_LEVEL_SIZE = 1;
 
     // With the English main dictionary as of October 2013, the size of bigram address table is
     // is 345KB with the block size being 16.
     // This is 54% of that of full address table.
     static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16;
-    static final int BIGRAM_CONTENT_COUNT = 2;
+    static final int BIGRAM_CONTENT_COUNT = 1;
     static final int BIGRAM_FREQ_CONTENT_INDEX = 0;
-    static final int BIGRAM_TIMESTAMP_CONTENT_INDEX = 1;
     static final String BIGRAM_FREQ_CONTENT_ID = "_freq";
-    static final String BIGRAM_TIMESTAMP_CONTENT_ID = "_timestamp";
     static final int BIGRAM_TIMESTAMP_SIZE = 4;
     static final int BIGRAM_COUNTER_SIZE = 1;
     static final int BIGRAM_LEVEL_SIZE = 1;
@@ -340,16 +336,19 @@
         public final int mBodyOffset;
         public final DictionaryOptions mDictionaryOptions;
         public final FormatOptions mFormatOptions;
+
         // Note that these are corresponding definitions in native code in latinime::HeaderPolicy
         // and latinime::HeaderReadWriteUtils.
-        public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE";
-        public static final String HAS_HISTORICAL_INFO_ATTRIBUTE = "HAS_HISTORICAL_INFO";
+        // TODO: Standardize the key names and bump up the format version, taking care not to
+        // break format version 2 dictionaries.
+        public static final String DICTIONARY_VERSION_KEY = "version";
+        public static final String DICTIONARY_LOCALE_KEY = "locale";
+        public static final String DICTIONARY_ID_KEY = "dictionary";
+        public static final String DICTIONARY_DESCRIPTION_KEY = "description";
+        public static final String DICTIONARY_DATE_KEY = "date";
+        public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO";
+        public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE";
         public static final String ATTRIBUTE_VALUE_TRUE = "1";
-
-        public static final String DICTIONARY_VERSION_ATTRIBUTE = "version";
-        public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale";
-        public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary";
-        private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description";
         public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
                 final FormatOptions formatOptions) throws UnsupportedFormatException {
             mDictionaryOptions = dictionaryOptions;
@@ -369,24 +368,24 @@
 
         // Helper method to get the locale as a String
         public String getLocaleString() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_KEY);
         }
 
         // Helper method to get the version String
         public String getVersion() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_KEY);
         }
 
         // Helper method to get the dictionary ID as a String
         public String getId() {
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_KEY);
         }
 
         // Helper method to get the description
         public String getDescription() {
             // TODO: Right now each dictionary file comes with a description in its own language.
             // It will display as is no matter the device's locale. It should be internationalized.
-            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE);
+            return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_KEY);
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index fdf2ae7..5b0e839 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -61,6 +61,7 @@
             mData = new ArrayList<PtNode>();
         }
         public PtNodeArray(ArrayList<PtNode> data) {
+            Collections.sort(data, PTNODE_COMPARATOR);
             mData = data;
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 7071893..e459e48 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -143,7 +143,7 @@
         mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer(
                 getFile(FILETYPE_TERMINAL_ADDRESS_TABLE));
         mBigramReader = new BigramContentReader(mDictDirectory.getName(),
-                mDictDirectory, mBufferFactory, false);
+                mDictDirectory, mBufferFactory);
         mBigramReader.openBuffers();
         mShortcutReader = new ShortcutContentReader(mDictDirectory.getName(), mDictDirectory,
                 mBufferFactory);
@@ -184,39 +184,24 @@
      */
     protected static class BigramContentReader extends SparseTableContentReader {
         public BigramContentReader(final String name, final File baseDir,
-                final DictionaryBufferFactory factory, final boolean hasTimestamp) {
+                final DictionaryBufferFactory factory) {
             super(name + FormatSpec.BIGRAM_FILE_EXTENSION,
                     FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
-                    getContentFilenames(name, hasTimestamp), getContentIds(hasTimestamp), factory);
+                    getContentFilenames(name), getContentIds(), factory);
         }
 
         // TODO: Consolidate this method and BigramContentWriter.getContentFilenames.
-        protected static String[] getContentFilenames(final String name,
-                final boolean hasTimestamp) {
-            final String[] contentFilenames;
-            if (hasTimestamp) {
-                contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION,
-                        name + FormatSpec.BIGRAM_FILE_EXTENSION };
-            } else {
-                contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION };
-            }
-            return contentFilenames;
+        protected static String[] getContentFilenames(final String name) {
+            return new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION };
         }
 
         // TODO: Consolidate this method and BigramContentWriter.getContentIds.
-        protected static String[] getContentIds(final boolean hasTimestamp) {
-            final String[] contentIds;
-            if (hasTimestamp) {
-                contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID,
-                        FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID };
-            } else {
-                contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID };
-            }
-            return contentIds;
+        protected static String[] getContentIds() {
+            return new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID };
         }
 
         public ArrayList<PendingAttribute> readTargetsAndFrequencies(final int terminalId,
-                final DictBuffer terminalAddressTableBuffer) {
+                final DictBuffer terminalAddressTableBuffer, final FormatOptions options) {
             final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList();
             read(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId,
                     new SparseTableContentReaderInterface() {
@@ -226,14 +211,26 @@
                                 // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE,
                                 // remaining bigram entries are ignored.
                                 final int bigramFlags = buffer.readUnsignedByte();
+                                final int probability;
+
+                                if (options.mHasTimestamp) {
+                                    probability = buffer.readUnsignedByte();
+                                    // Skip timestamp
+                                    buffer.readInt();
+                                    // Skip level
+                                    buffer.readUnsignedByte();
+                                    // Skip count
+                                    buffer.readUnsignedByte();
+                                } else {
+                                    probability = bigramFlags
+                                            & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
+                                }
                                 final int targetTerminalId = buffer.readUnsignedInt24();
                                 terminalAddressTableBuffer.position(targetTerminalId
                                         * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE);
                                 final int targetAddress =
                                         terminalAddressTableBuffer.readUnsignedInt24();
-                                bigrams.add(new PendingAttribute(bigramFlags
-                                        & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
-                                        targetAddress));
+                                bigrams.add(new PendingAttribute(probability, targetAddress));
                                 if (0 == (bigramFlags
                                         & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) {
                                     break;
@@ -286,8 +283,19 @@
     }
 
     protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader {
-        protected static int readFrequency(final DictBuffer frequencyBuffer, final int terminalId) {
-            frequencyBuffer.position(terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE + 1);
+        protected static int readFrequency(final DictBuffer frequencyBuffer, final int terminalId,
+                final FormatOptions formatOptions) {
+            final int readingPos;
+            if (formatOptions.mHasTimestamp) {
+                final int entrySize = FormatSpec.FREQUENCY_AND_FLAGS_SIZE
+                        + FormatSpec.UNIGRAM_TIMESTAMP_SIZE + FormatSpec.UNIGRAM_LEVEL_SIZE
+                        + FormatSpec.UNIGRAM_COUNTER_SIZE;
+                readingPos = terminalId * entrySize + FormatSpec.FLAGS_IN_FREQ_FILE_SIZE;
+            } else {
+                readingPos = terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE
+                        + FormatSpec.FLAGS_IN_FREQ_FILE_SIZE;
+            }
+            frequencyBuffer.position(readingPos);
             return frequencyBuffer.readUnsignedByte();
         }
 
@@ -354,12 +362,12 @@
     }
 
     @Override
-    public PtNodeInfo readPtNode(int ptNodePos, FormatOptions options) {
+    public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions options) {
         final Ver4PtNodeInfo nodeInfo = readVer4PtNodeInfo(ptNodePos, options);
 
         final int frequency;
         if (0 != (FormatSpec.FLAG_IS_TERMINAL & nodeInfo.mFlags)) {
-            frequency = PtNodeReader.readFrequency(mFrequencyBuffer, nodeInfo.mTerminalId);
+            frequency = PtNodeReader.readFrequency(mFrequencyBuffer, nodeInfo.mTerminalId, options);
         } else {
             frequency = PtNode.NOT_A_TERMINAL;
         }
@@ -367,7 +375,7 @@
         final ArrayList<WeightedString> shortcutTargets = mShortcutReader.readShortcuts(
                 nodeInfo.mTerminalId);
         final ArrayList<PendingAttribute> bigrams = mBigramReader.readTargetsAndFrequencies(
-                nodeInfo.mTerminalId, mTerminalAddressTableBuffer);
+                nodeInfo.mTerminalId, mTerminalAddressTableBuffer, options);
 
         return new PtNodeInfo(ptNodePos, ptNodePos + nodeInfo.mNodeSize, nodeInfo.mFlags,
                 nodeInfo.mCharacters, frequency, nodeInfo.mParentPos, nodeInfo.mChildrenPos,
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index d34aa17..b12f79b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -62,7 +62,7 @@
         final BinaryDictionary binaryDict = new BinaryDictionary(mDictPlacedDir.getAbsolutePath(),
                 0l, mDictPlacedDir.length(), true /* useFullEditDistance */,
                 LocaleUtils.constructLocaleFromString(dict.mOptions.mAttributes.get(
-                        FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE)),
+                        FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY)),
                 Dictionary.TYPE_USER /* Dictionary type. Does not matter for us */,
                 true /* isUpdatable */);
         if (!binaryDict.isValidDictionary()) {
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 4a610e6..d636a25 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -95,13 +95,13 @@
     @Override
     protected Map<String, String> getHeaderAttributeMap() {
         HashMap<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mDictName);
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale.toString());
-        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, mDictName);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, mLocale.toString());
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         return attributeMap;
     }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index e4ae64f..2979544 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -49,7 +49,6 @@
     // From resources:
     public final SpacingAndPunctuations mSpacingAndPunctuations;
     public final int mDelayUpdateOldSuggestions;
-    public final CharSequence mHintToSaveText;
 
     // From preferences, in the same order as xml/prefs.xml:
     public final boolean mAutoCap;
@@ -101,7 +100,6 @@
         // Get the resources
         mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions);
         mSpacingAndPunctuations = new SpacingAndPunctuations(res);
-        mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
 
         // Store the input attributes
         if (null == inputAttributes) {
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 3cd9d95..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,11 +44,13 @@
 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;
 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
 import com.android.inputmethod.latin.utils.ResourceUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 import com.android.inputmethod.latin.utils.ViewLayoutUtils;
 
 import java.util.ArrayList;
@@ -459,7 +460,7 @@
     }
 
     public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip,
-            final int stripWidth, final CharSequence hintText) {
+            final int stripWidth) {
         final int width = stripWidth - mDividerWidth - mPadding * 2;
 
         final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save);
@@ -473,13 +474,17 @@
 
         final TextView hintView = (TextView)addToDictionaryStrip.findViewById(
                 R.id.hint_add_to_dictionary);
-        hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL | GravityCompat.START);
         hintView.setTextColor(mColorAutoCorrect);
         final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip)
                 == ViewCompat.LAYOUT_DIRECTION_RTL);
-        final String hintWithArrow = (isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW)
-                + hintText;
+        final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW;
+        final Resources res = addToDictionaryStrip.getResources();
+        final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(res.getConfiguration().locale);
+        final CharSequence hintText = res.getText(R.string.hint_add_to_dictionary);
+        final String hintWithArrow = (isRtlLanguage == isRtlSystem)
+                ? (arrow + hintText) : (hintText + arrow);
         final int hintWidth = width - wordWidth;
+        hintView.setTextScaleX(1.0f); // Reset textScaleX.
         final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint());
         hintView.setText(hintWithArrow);
         hintView.setTextScaleX(hintScaleX);
@@ -487,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 0ebf5cb..fe95d67 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Color;
 import android.support.v4.view.ViewCompat;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.GestureDetector;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -52,13 +54,16 @@
         OnLongClickListener {
     public interface Listener {
         public void addWordToUserDictionary(String word);
+        public void showImportantNoticeContents();
         public void pickSuggestionManually(int index, SuggestedWordInfo word);
     }
 
     static final boolean DBG = LatinImeLogger.sDBG;
+    private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f;
 
     private final ViewGroup mSuggestionsStrip;
     private final ViewGroup mAddToDictionaryStrip;
+    private final View mImportantNoticeStrip;
     MainKeyboardView mMainKeyboardView;
 
     private final View mMoreSuggestionsContainer;
@@ -78,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();
         }
 
@@ -90,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() {
@@ -125,17 +142,22 @@
 
         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 = (TextView)inflater.inflate(R.layout.suggestion_word, null);
+            final TextView word = new TextView(context, null, R.attr.suggestionWordStyle);
             word.setOnClickListener(this);
             word.setOnLongClickListener(this);
             mWordViews.add(word);
             final View divider = inflater.inflate(R.layout.suggestion_divider, null);
             divider.setOnClickListener(this);
             mDividerViews.add(divider);
-            mDebugInfoViews.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
+            final TextView info = new TextView(context, null, R.attr.suggestionWordStyle);
+            info.setTextColor(Color.WHITE);
+            info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP);
+            mDebugInfoViews.add(info);
         }
 
         mLayoutHelper = new SuggestionStripLayoutHelper(
@@ -170,6 +192,7 @@
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
         }
+        mStripVisibilityGroup.showSuggestionsStrip();
     }
 
     public int setMoreSuggestionsHeight(final int remainingHeight) {
@@ -180,8 +203,8 @@
         return mStripVisibilityGroup.isShowingAddToDictionaryStrip();
     }
 
-    public void showAddToDictionaryHint(final String word, final CharSequence hintText) {
-        mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, getWidth(), hintText);
+    public void showAddToDictionaryHint(final String word) {
+        mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, getWidth());
         // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
         // will be extracted at {@link #onClick(View)}.
         mAddToDictionaryStrip.setTag(word);
@@ -197,6 +220,12 @@
         return false;
     }
 
+    public void showImportantNoticeTitle() {
+        mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, getWidth());
+        mStripVisibilityGroup.showImportantNoticeStrip();
+        mImportantNoticeStrip.setOnClickListener(this);
+    }
+
     public void clear() {
         mSuggestionsStrip.removeAllViews();
         removeAllDebugInfoViews();
@@ -354,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/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index cc25102..bbfa0f0 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -102,4 +102,19 @@
     public static <E> SparseArray<E> newSparseArray() {
         return new SparseArray<E>();
     }
+
+    public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) {
+        if (array == null) {
+            throw new NullPointerException();
+        }
+        if (start < 0 || start > end || end > array.length) {
+            throw new IllegalArgumentException();
+        }
+
+        final ArrayList<E> list = newArrayList(end - start);
+        for (int i = start; i < end; i++) {
+            list.add(array[i]);
+        }
+        return list;
+    }
 }
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/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
new file mode 100644
index 0000000..4a08231
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.util.Log;
+
+import com.android.inputmethod.latin.R;
+
+public final class ImportantNoticeUtils {
+    private static final String TAG = ImportantNoticeUtils.class.getSimpleName();
+
+    // {@link SharedPreferences} name to save the last important notice version that has been
+    // displayed to users.
+    private static final String PREFERENCE_NAME = "important_notice";
+    private static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version";
+
+    // Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key.
+    // The value is zero until each multiuser completes system setup wizard.
+    // Caveat: This is a hidden API.
+    private static final String Settings_Secure_USER_SETUP_COMPLETE = "user_setup_complete";
+    private static final int USER_SETUP_IS_NOT_COMPLETE = 0;
+
+    private ImportantNoticeUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static boolean isInSystemSetupWizard(final Context context) {
+        try {
+            final int userSetupComplete = Settings.Secure.getInt(
+                    context.getContentResolver(), Settings_Secure_USER_SETUP_COMPLETE);
+            return userSetupComplete == USER_SETUP_IS_NOT_COMPLETE;
+        } catch (final SettingNotFoundException e) {
+            Log.w(TAG, "Can't find settings in Settings.Secure: key="
+                    + Settings_Secure_USER_SETUP_COMPLETE);
+            return false;
+        }
+    }
+
+    private static SharedPreferences getImportantNoticePreferences(final Context context) {
+        return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
+    }
+
+    private static int getCurrentImportantNoticeVersion(final Context context) {
+        return context.getResources().getInteger(R.integer.config_important_notice_version);
+    }
+
+    public static boolean hasNewImportantNotice(final Context context) {
+        final SharedPreferences prefs = getImportantNoticePreferences(context);
+        final int lastVersion = prefs.getInt(KEY_IMPORTANT_NOTICE_VERSION, 0);
+        return getCurrentImportantNoticeVersion(context) > lastVersion;
+    }
+
+    public static void updateLastImportantNoticeVersion(final Context context) {
+        final SharedPreferences prefs = getImportantNoticePreferences(context);
+        prefs.edit()
+                .putInt(KEY_IMPORTANT_NOTICE_VERSION, getCurrentImportantNoticeVersion(context))
+                .apply();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index b154623..c632a71 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.utils;
 
+import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
+
 import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
@@ -471,4 +473,20 @@
         }
         return bytes;
     }
+
+    public static String toUpperCaseOfStringForLocale(final String text,
+            final boolean needsToUpperCase, final Locale locale) {
+        if (text == null || !needsToUpperCase) return text;
+        return text.toUpperCase(locale);
+    }
+
+    public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase,
+            final Locale locale) {
+        if (!Constants.isLetterCode(code) || !needsToUpperCase) return code;
+        final String text = newSingleCodePointString(code);
+        final String casedText = toUpperCaseOfStringForLocale(
+                text, needsToUpperCase, locale);
+        return codePointCount(casedText) == 1
+                ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 0d02889..b3871bf 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -347,9 +347,12 @@
         Arrays.sort(SORTED_RTL_LANGUAGES);
     }
 
-    public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
-        final Locale locale = getSubtypeLocale(subtype);
+    public static boolean isRtlLanguage(final Locale locale) {
         final String language = locale.getLanguage();
         return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0;
     }
+
+    public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
+        return isRtlLanguage(getSubtypeLocale(subtype));
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java b/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java
deleted file mode 100644
index 4feee43..0000000
--- a/java/src/com/android/inputmethod/latin/utils/UnigramProperty.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-
-package com.android.inputmethod.latin.utils;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.BinaryDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.util.ArrayList;
-
-// This has information that belong to a unigram. This class has some detailed attributes such as
-// historical information but they have to be checked only for testing purpose.
-@UsedForTesting
-public class UnigramProperty {
-    public final String mCodePoints;
-    public final boolean mIsNotAWord;
-    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 ArrayList<WeightedString> mShortcutTargets = CollectionUtils.newArrayList();
-
-    private static int getCodePointCount(final int[] codePoints) {
-        for (int i = 0; i < codePoints.length; i++) {
-            if (codePoints[i] == 0) {
-                return i;
-            }
-        }
-        return codePoints.length;
-    }
-
-    // This represents invalid unigram when the probability is BinaryDictionary.NOT_A_PROBABILITY.
-    public UnigramProperty(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 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;
-        final int shortcutTargetCount = shortcutTargets.size();
-        for (int i = 0; i < shortcutTargetCount; i++) {
-            final int[] shortcutTargetCodePointArray = shortcutTargets.get(i);
-            final String shortcutTargetString = new String(shortcutTargetCodePointArray,
-                    0 /* offset */, getCodePointCount(shortcutTargetCodePointArray));
-            mShortcutTargets.add(
-                    new WeightedString(shortcutTargetString, shortcutProbabilities.get(i)));
-        }
-    }
-
-    @UsedForTesting
-    public boolean isValid() {
-        return mProbability != BinaryDictionary.NOT_A_PROBABILITY;
-    }
-}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index db628fe..7af03da 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictEncoder;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
@@ -44,9 +45,6 @@
 public final class UserHistoryDictIOUtils {
     private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName();
     private static final boolean DEBUG = false;
-    private static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE";
-    private static final String USES_FORGETTING_CURVE_VALUE = "1";
-    private static final String LAST_UPDATED_TIME_KEY = "date";
 
     public interface OnAddWordListener {
         /**
@@ -75,8 +73,9 @@
             final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
             final FormatOptions formatOptions, final HashMap<String, String> options) {
         final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams, options);
-        fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE);
-        fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY,
+        fusionDict.addOptionAttribute(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
+            FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
+        fusionDict.addOptionAttribute(FormatSpec.FileHeader.DICTIONARY_DATE_KEY,
                 String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
         try {
             dictEncoder.writeDictionary(fusionDict, formatOptions);
diff --git a/java/src/com/android/inputmethod/latin/utils/WordProperty.java b/java/src/com/android/inputmethod/latin/utils/WordProperty.java
new file mode 100644
index 0000000..ba9b114
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/WordProperty.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.inputmethod.latin.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.util.ArrayList;
+
+// This has information that belong to a unigram. This class has some detailed attributes such as
+// historical information but they have to be checked only for testing purpose.
+@UsedForTesting
+public class WordProperty {
+    public final String mCodePoints;
+    public final boolean mIsNotAWord;
+    public final boolean mIsBlacklisted;
+    public final boolean mHasBigrams;
+    public final boolean mHasShortcuts;
+    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) {
+                return i;
+            }
+        }
+        return codePoints.length;
+    }
+
+    // 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[] 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;
+        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);
+            final String shortcutTargetString = new String(shortcutTargetCodePointArray,
+                    0 /* offset */, getCodePointCount(shortcutTargetCodePointArray));
+            mShortcutTargets.add(
+                    new WeightedString(shortcutTargetString, shortcutProbabilities.get(i)));
+        }
+    }
+
+    @UsedForTesting
+    public boolean isValid() {
+        return mProbabilityInfo.mProbability != BinaryDictionary.NOT_A_PROBABILITY;
+    }
+}
\ No newline at end of file
diff --git a/native/jni/NativeFileList.mk b/native/jni/NativeFileList.mk
index d2f2259..c7061c8 100644
--- a/native/jni/NativeFileList.mk
+++ b/native/jni/NativeFileList.mk
@@ -32,7 +32,7 @@
         error_type_utils.cpp \
         multi_bigram_map.cpp \
         suggestions_output_utils.cpp \
-        unigram_property.cpp) \
+        word_property.cpp) \
     $(addprefix suggest/core/layout/, \
         additional_proximity_chars.cpp \
         proximity_info.cpp \
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 586a306..8f3f8e2 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -24,7 +24,7 @@
 #include "jni.h"
 #include "jni_common.h"
 #include "suggest/core/dictionary/dictionary.h"
-#include "suggest/core/dictionary/unigram_property.h"
+#include "suggest/core/dictionary/word_property.h"
 #include "suggest/core/suggest_options.h"
 #include "suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.h"
 #include "suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h"
@@ -260,19 +260,19 @@
             word1Length);
 }
 
-static void latinime_BinaryDictionary_getUnigramProperty(JNIEnv *env, jclass clazz,
+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 UnigramProperty unigramProperty = dictionary->getUnigramProperty(
-            wordCodePoints, wordLength);
-    unigramProperty.outputProperties(env, outCodePoints, outFlags, outProbability,
-            outHistoricalInfo, outShortcutTargets, outShortcutProbabilities);
+    const WordProperty wordProperty = dictionary->getWordProperty(wordCodePoints, wordLength);
+    wordProperty.outputProperties(env, outCodePoints, outFlags, outProbabilityInfo,
+            outBigramTargets, outBigramProbabilityInfo, outShortcutTargets,
+            outShortcutProbabilities);
 }
 
 static jfloat latinime_BinaryDictionary_calcNormalizedScore(JNIEnv *env, jclass clazz,
@@ -521,9 +521,10 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_getBigramProbability)
     },
     {
-        const_cast<char *>("getUnigramPropertyNative"),
-        const_cast<char *>("(J[I[I[Z[I[ILjava/util/ArrayList;Ljava/util/ArrayList;)V"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_getUnigramProperty)
+        const_cast<char *>("getWordPropertyNative"),
+        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 *>("calcNormalizedScoreNative"),
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index e68c0a6..16b1a56 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -143,10 +143,10 @@
             maxResultLength);
 }
 
-const UnigramProperty Dictionary::getUnigramProperty(const int *const codePoints,
+const WordProperty Dictionary::getWordProperty(const int *const codePoints,
         const int codePointCount) {
     TimeKeeper::setCurrentTime();
-    return mDictionaryStructureWithBufferPolicy.get()->getUnigramProperty(
+    return mDictionaryStructureWithBufferPolicy.get()->getWordProperty(
             codePoints, codePointCount);
 }
 
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index b37b4aa..4a468f3 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -22,7 +22,7 @@
 #include "defines.h"
 #include "jni.h"
 #include "suggest/core/dictionary/bigram_dictionary.h"
-#include "suggest/core/dictionary/unigram_property.h"
+#include "suggest/core/dictionary/word_property.h"
 #include "suggest/core/policy/dictionary_header_structure_policy.h"
 #include "suggest/core/policy/dictionary_structure_with_buffer_policy.h"
 #include "suggest/core/suggest_interface.h"
@@ -34,7 +34,7 @@
 class DicTraverseSession;
 class ProximityInfo;
 class SuggestOptions;
-class UnigramProperty;
+class WordProperty;
 
 class Dictionary {
  public:
@@ -94,7 +94,7 @@
     void getProperty(const char *const query, const int queryLength, char *const outResult,
             const int maxResultLength);
 
-    const UnigramProperty getUnigramProperty(const int *const codePoints, const int codePointCount);
+    const WordProperty getWordProperty(const int *const codePoints, const int codePointCount);
 
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const {
         return mDictionaryStructureWithBufferPolicy.get();
diff --git a/native/jni/src/suggest/core/dictionary/unigram_property.h b/native/jni/src/suggest/core/dictionary/unigram_property.h
deleted file mode 100644
index c4ebb86..0000000
--- a/native/jni/src/suggest/core/dictionary/unigram_property.h
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef LATINIME_UNIGRAM_PROPERTY_H
-#define LATINIME_UNIGRAM_PROPERTY_H
-
-#include <cstring>
-#include <vector>
-
-#include "defines.h"
-#include "jni.h"
-
-namespace latinime {
-
-// This class is used for returning information belonging to a unigram to java side.
-class UnigramProperty {
- public:
-    // Invalid unigram.
-    UnigramProperty()
-            : mCodePoints(), mCodePointCount(0), mIsNotAWord(false), mIsBlacklisted(false),
-              mHasBigrams(false), mHasShortcuts(false), mProbability(NOT_A_PROBABILITY),
-              mTimestamp(0), mLevel(0), mCount(0), mShortcutTargets(), mShortcutProbabilities() {}
-
-    UnigramProperty(const UnigramProperty &unigramProperty)
-            : mCodePoints(), mCodePointCount(unigramProperty.mCodePointCount),
-              mIsNotAWord(unigramProperty.mIsNotAWord),
-              mIsBlacklisted(unigramProperty.mIsBlacklisted),
-              mHasBigrams(unigramProperty.mHasBigrams),
-              mHasShortcuts(unigramProperty.mHasShortcuts),
-              mProbability(unigramProperty.mProbability),
-              mTimestamp(unigramProperty.mTimestamp), mLevel(unigramProperty.mLevel),
-              mCount(unigramProperty.mCount), mShortcutTargets(unigramProperty.mShortcutTargets),
-              mShortcutProbabilities(unigramProperty.mShortcutProbabilities) {
-        memcpy(mCodePoints, unigramProperty.mCodePoints, sizeof(mCodePoints));
-    }
-
-    UnigramProperty(const int *const codePoints, const int codePointCount,
-            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)
-            : mCodePoints(), mCodePointCount(codePointCount),
-              mIsNotAWord(isNotAWord), mIsBlacklisted(isBlacklisted), mHasBigrams(hasBigrams),
-              mHasShortcuts(hasShortcuts), mProbability(probability), mTimestamp(timestamp),
-              mLevel(level), mCount(count), mShortcutTargets(*shortcutTargets),
-              mShortcutProbabilities(*shortcutProbabilities) {
-        memcpy(mCodePoints, codePoints, sizeof(mCodePoints));
-    }
-
-    void outputProperties(JNIEnv *const env, jintArray outCodePoints, jbooleanArray outFlags,
-            jintArray outProbability, jintArray outHistoricalInfo, jobject outShortcutTargets,
-            jobject outShortcutProbabilities) const;
-
- private:
-    DISALLOW_ASSIGNMENT_OPERATOR(UnigramProperty);
-
-    int mCodePoints[MAX_WORD_LENGTH];
-    int mCodePointCount;
-    bool mIsNotAWord;
-    bool mIsBlacklisted;
-    bool mHasBigrams;
-    bool mHasShortcuts;
-    int mProbability;
-    // Historical information
-    int mTimestamp;
-    int mLevel;
-    int mCount;
-    // Shortcut
-    std::vector<std::vector<int> > mShortcutTargets;
-    std::vector<int> mShortcutProbabilities;
-};
-} // namespace latinime
-#endif // LATINIME_UNIGRAM_PROPERTY_H
diff --git a/native/jni/src/suggest/core/dictionary/unigram_property.cpp b/native/jni/src/suggest/core/dictionary/word_property.cpp
similarity index 67%
rename from native/jni/src/suggest/core/dictionary/unigram_property.cpp
rename to native/jni/src/suggest/core/dictionary/word_property.cpp
index 16bbb69..d8c330b 100644
--- a/native/jni/src/suggest/core/dictionary/unigram_property.cpp
+++ b/native/jni/src/suggest/core/dictionary/word_property.cpp
@@ -14,34 +14,38 @@
  * limitations under the License.
  */
 
-#include "suggest/core/dictionary/unigram_property.h"
+#include "suggest/core/dictionary/word_property.h"
 
 namespace latinime {
 
-void UnigramProperty::outputProperties(JNIEnv *const env, jintArray outCodePoints,
-        jbooleanArray outFlags, jintArray outProbability, jintArray outHistoricalInfo,
-        jobject outShortcutTargets, jobject outShortcutProbabilities) const {
-    env->SetIntArrayRegion(outCodePoints, 0 /* start */, mCodePointCount, mCodePoints);
+void WordProperty::outputProperties(JNIEnv *const env, jintArray outCodePoints,
+        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();
+
+    // TODO: Output bigrams.
+    // 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
new file mode 100644
index 0000000..cc06b1b
--- /dev/null
+++ b/native/jni/src/suggest/core/dictionary/word_property.h
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LATINIME_WORD_PROPERTY_H
+#define LATINIME_WORD_PROPERTY_H
+
+#include <cstring>
+#include <vector>
+
+#include "defines.h"
+#include "jni.h"
+
+namespace latinime {
+
+// This class is used for returning information belonging to a word to java side.
+class WordProperty {
+ public:
+    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) {}
+
+     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), 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<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), mBigrams(*bigrams),
+              mShortcuts(*shortcuts) {}
+
+    void outputProperties(JNIEnv *const env, jintArray outCodePoints, jbooleanArray outFlags,
+            jintArray outProbabilityInfo, jobject outBigramTargets, jobject outBigramProbabilities,
+            jobject outShortcutTargets, jobject outShortcutProbabilities) const;
+
+ private:
+    DISALLOW_ASSIGNMENT_OPERATOR(WordProperty);
+
+    std::vector<int> mCodePoints;
+    bool mIsNotAWord;
+    bool mIsBlacklisted;
+    bool mHasBigrams;
+    bool mHasShortcuts;
+    int mProbability;
+    // Historical information
+    int mTimestamp;
+    int mLevel;
+    int mCount;
+    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 c74a4eb..b878984 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
@@ -18,7 +18,7 @@
 #define LATINIME_DICTIONARY_STRUCTURE_POLICY_H
 
 #include "defines.h"
-#include "suggest/core/dictionary/unigram_property.h"
+#include "suggest/core/dictionary/word_property.h"
 #include "utils/exclusive_ownership_pointer.h"
 
 namespace latinime {
@@ -92,7 +92,7 @@
             const int maxResultLength) = 0;
 
     // Used for testing.
-    virtual const UnigramProperty getUnigramProperty(const int *const codePonts,
+    virtual const WordProperty getWordProperty(const int *const codePonts,
             const int codePointCount) const = 0;
 
  protected:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp
index be7a3c2..7504524 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp
@@ -24,7 +24,7 @@
         "REQUIRES_GERMAN_UMLAUT_PROCESSING";
 // TODO: Change attribute string to "IS_DECAYING_DICT".
 const char *const HeaderPolicy::IS_DECAYING_DICT_KEY = "USES_FORGETTING_CURVE";
-const char *const HeaderPolicy::LAST_UPDATED_TIME_KEY = "date";
+const char *const HeaderPolicy::DATE_KEY = "date";
 const char *const HeaderPolicy::LAST_DECAYED_TIME_KEY = "LAST_DECAYED_TIME";
 const char *const HeaderPolicy::UNIGRAM_COUNT_KEY = "UNIGRAM_COUNT";
 const char *const HeaderPolicy::BIGRAM_COUNT_KEY = "BIGRAM_COUNT";
@@ -73,51 +73,56 @@
             REQUIRES_GERMAN_UMLAUT_PROCESSING_KEY, false);
 }
 
-bool HeaderPolicy::writeHeaderToBuffer(BufferWithExtendableBuffer *const bufferToWrite,
-        const bool updatesLastUpdatedTime, const bool updatesLastDecayedTime,
-        const int unigramCount, const int bigramCount, const int extendedRegionSize) const {
+bool HeaderPolicy::fillInAndWriteHeaderToBuffer(const bool updatesLastDecayedTime,
+        const int unigramCount, const int bigramCount,
+        const int extendedRegionSize, BufferWithExtendableBuffer *const outBuffer) const {
     int writingPos = 0;
-    if (!HeaderReadWriteUtils::writeDictionaryVersion(bufferToWrite, mDictFormatVersion,
+    HeaderReadWriteUtils::AttributeMap attributeMapToWrite(mAttributeMap);
+    fillInHeader(updatesLastDecayedTime, unigramCount, bigramCount,
+            extendedRegionSize, &attributeMapToWrite);
+    if (!HeaderReadWriteUtils::writeDictionaryVersion(outBuffer, mDictFormatVersion,
             &writingPos)) {
         return false;
     }
-    if (!HeaderReadWriteUtils::writeDictionaryFlags(bufferToWrite, mDictionaryFlags,
+    if (!HeaderReadWriteUtils::writeDictionaryFlags(outBuffer, mDictionaryFlags,
             &writingPos)) {
         return false;
     }
     // Temporarily writes a dummy header size.
     int headerSizeFieldPos = writingPos;
-    if (!HeaderReadWriteUtils::writeDictionaryHeaderSize(bufferToWrite, 0 /* size */,
+    if (!HeaderReadWriteUtils::writeDictionaryHeaderSize(outBuffer, 0 /* size */,
             &writingPos)) {
         return false;
     }
-    HeaderReadWriteUtils::AttributeMap attributeMapTowrite(mAttributeMap);
-    HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, UNIGRAM_COUNT_KEY, unigramCount);
-    HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, BIGRAM_COUNT_KEY, bigramCount);
-    HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, EXTENDED_REGION_SIZE_KEY,
-            extendedRegionSize);
-    if (updatesLastUpdatedTime) {
-        // Set current time as a last updated time.
-        HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, LAST_UPDATED_TIME_KEY,
-                TimeKeeper::peekCurrentTime());
-    }
-    if (updatesLastDecayedTime) {
-        // Set current time as a last updated time.
-        HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, LAST_DECAYED_TIME_KEY,
-                TimeKeeper::peekCurrentTime());
-    }
-    if (!HeaderReadWriteUtils::writeHeaderAttributes(bufferToWrite, &attributeMapTowrite,
+    if (!HeaderReadWriteUtils::writeHeaderAttributes(outBuffer, &attributeMapToWrite,
             &writingPos)) {
         return false;
     }
-    // Writes an actual header size.
-    if (!HeaderReadWriteUtils::writeDictionaryHeaderSize(bufferToWrite, writingPos,
+    // Writes the actual header size.
+    if (!HeaderReadWriteUtils::writeDictionaryHeaderSize(outBuffer, writingPos,
             &headerSizeFieldPos)) {
         return false;
     }
     return true;
 }
 
+void HeaderPolicy::fillInHeader(const bool updatesLastDecayedTime, const int unigramCount,
+        const int bigramCount, const int extendedRegionSize,
+        HeaderReadWriteUtils::AttributeMap *outAttributeMap) const {
+    HeaderReadWriteUtils::setIntAttribute(outAttributeMap, UNIGRAM_COUNT_KEY, unigramCount);
+    HeaderReadWriteUtils::setIntAttribute(outAttributeMap, BIGRAM_COUNT_KEY, bigramCount);
+    HeaderReadWriteUtils::setIntAttribute(outAttributeMap, EXTENDED_REGION_SIZE_KEY,
+            extendedRegionSize);
+    // Set the current time as the generation time.
+    HeaderReadWriteUtils::setIntAttribute(outAttributeMap, DATE_KEY,
+            TimeKeeper::peekCurrentTime());
+    if (updatesLastDecayedTime) {
+        // Set current time as the last updated time.
+        HeaderReadWriteUtils::setIntAttribute(outAttributeMap, LAST_DECAYED_TIME_KEY,
+                TimeKeeper::peekCurrentTime());
+    }
+}
+
 /* static */ HeaderReadWriteUtils::AttributeMap
         HeaderPolicy::createAttributeMapAndReadAllAttributes(const uint8_t *const dictBuf) {
     HeaderReadWriteUtils::AttributeMap attributeMap;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
index 1208d2c..a44f9f0 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
@@ -39,8 +39,8 @@
               mRequiresGermanUmlautProcessing(readRequiresGermanUmlautProcessing()),
               mIsDecayingDict(HeaderReadWriteUtils::readBoolAttributeValue(&mAttributeMap,
                       IS_DECAYING_DICT_KEY, false /* defaultValue */)),
-              mLastUpdatedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
-                      LAST_UPDATED_TIME_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
+              mDate(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
+                      DATE_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
               mLastDecayedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
                       LAST_DECAYED_TIME_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
               mUnigramCount(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
@@ -62,10 +62,10 @@
               mRequiresGermanUmlautProcessing(readRequiresGermanUmlautProcessing()),
               mIsDecayingDict(HeaderReadWriteUtils::readBoolAttributeValue(&mAttributeMap,
                       IS_DECAYING_DICT_KEY, false /* defaultValue */)),
-              mLastUpdatedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
-                      LAST_UPDATED_TIME_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
+              mDate(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
+                      DATE_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
               mLastDecayedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
-                      LAST_UPDATED_TIME_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
+                      DATE_KEY, TimeKeeper::peekCurrentTime() /* defaultValue */)),
               mUnigramCount(0), mBigramCount(0), mExtendedRegionSize(0),
               mHasHistoricalInfoOfWords(HeaderReadWriteUtils::readBoolAttributeValue(
                       &mAttributeMap, HAS_HISTORICAL_INFO_KEY, false /* defaultValue */)) {}
@@ -75,7 +75,7 @@
             : mDictFormatVersion(FormatUtils::UNKNOWN_VERSION), mDictionaryFlags(0), mSize(0),
               mAttributeMap(), mMultiWordCostMultiplier(0.0f),
               mRequiresGermanUmlautProcessing(false), mIsDecayingDict(false),
-              mLastUpdatedTime(0), mLastDecayedTime(0), mUnigramCount(0), mBigramCount(0),
+              mDate(0), mLastDecayedTime(0), mUnigramCount(0), mBigramCount(0),
               mExtendedRegionSize(0), mHasHistoricalInfoOfWords(false) {}
 
     ~HeaderPolicy() {}
@@ -122,8 +122,8 @@
         return mRequiresGermanUmlautProcessing;
     }
 
-    AK_FORCE_INLINE int getLastUpdatedTime() const {
-        return mLastUpdatedTime;
+    AK_FORCE_INLINE int getDate() const {
+        return mDate;
     }
 
     AK_FORCE_INLINE int getLastDecayedTime() const {
@@ -149,9 +149,13 @@
     void readHeaderValueOrQuestionMark(const char *const key,
             int *outValue, int outValueSize) const;
 
-    bool writeHeaderToBuffer(BufferWithExtendableBuffer *const bufferToWrite,
-            const bool updatesLastUpdatedTime, const bool updatesLastDecayedTime,
-            const int unigramCount, const int bigramCount, const int extendedRegionSize) const;
+    bool fillInAndWriteHeaderToBuffer(const bool updatesLastDecayedTime,
+            const int unigramCount, const int bigramCount,
+            const int extendedRegionSize, BufferWithExtendableBuffer *const outBuffer) const;
+
+    void fillInHeader(const bool updatesLastDecayedTime,
+            const int unigramCount, const int bigramCount, const int extendedRegionSize,
+            HeaderReadWriteUtils::AttributeMap *outAttributeMap) const;
 
  private:
     DISALLOW_COPY_AND_ASSIGN(HeaderPolicy);
@@ -159,7 +163,7 @@
     static const char *const MULTIPLE_WORDS_DEMOTION_RATE_KEY;
     static const char *const REQUIRES_GERMAN_UMLAUT_PROCESSING_KEY;
     static const char *const IS_DECAYING_DICT_KEY;
-    static const char *const LAST_UPDATED_TIME_KEY;
+    static const char *const DATE_KEY;
     static const char *const LAST_DECAYED_TIME_KEY;
     static const char *const UNIGRAM_COUNT_KEY;
     static const char *const BIGRAM_COUNT_KEY;
@@ -175,7 +179,7 @@
     const float mMultiWordCostMultiplier;
     const bool mRequiresGermanUmlautProcessing;
     const bool mIsDecayingDict;
-    const int mLastUpdatedTime;
+    const int mDate;
     const int mLastDecayedTime;
     const int mUnigramCount;
     const int mBigramCount;
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 2adafd2..67d615e 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
@@ -123,10 +123,10 @@
         }
     }
 
-    const UnigramProperty getUnigramProperty(const int *const codePoints,
+    const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const {
-        // getUnigramProperty is not supported.
-        return UnigramProperty();
+        // getWordProperty is not supported.
+        return WordProperty();
     }
 
  private:
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 b4730fe..5022baf 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
@@ -20,7 +20,7 @@
 
 #include "suggest/core/dicnode/dic_node.h"
 #include "suggest/core/dicnode/dic_node_vector.h"
-#include "suggest/core/dictionary/unigram_property.h"
+#include "suggest/core/dictionary/word_property.h"
 #include "suggest/policyimpl/dictionary/structure/pt_common/dynamic_pt_reading_helper.h"
 #include "suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_reader.h"
 #include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
@@ -317,22 +317,59 @@
     }
 }
 
-const UnigramProperty Ver4PatriciaTriePolicy::getUnigramProperty(const int *const codePoints,
+const WordProperty Ver4PatriciaTriePolicy::getWordProperty(const int *const codePoints,
         const int codePointCount) const {
     const int ptNodePos = getTerminalPtNodePositionOfWord(codePoints, codePointCount,
             false /* forceLowerCaseSearch */);
     if (ptNodePos == NOT_A_DICT_POS) {
-        AKLOGE("fetchUnigramProperty is called for invalid word.");
-        return UnigramProperty();
+        AKLOGE("getWordProperty is called for invalid word.");
+        return WordProperty();
     }
     const PtNodeParams ptNodeParams = mNodeReader.fetchNodeInfoInBufferFromPtNodePos(ptNodePos);
+    std::vector<int> codePointVector(ptNodeParams.getCodePoints(),
+            ptNodeParams.getCodePoints() + ptNodeParams.getCodePointCount());
     const ProbabilityEntry probabilityEntry =
             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];
@@ -345,15 +382,14 @@
             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 UnigramProperty(ptNodeParams.getCodePoints(), ptNodeParams.getCodePointCount(),
-            ptNodeParams.isNotAWord(), ptNodeParams.isBlacklisted(), ptNodeParams.hasBigrams(),
+    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);
 }
 
 } // 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 81aed20..a43bd0e 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
@@ -106,7 +106,7 @@
     void getProperty(const char *const query, const int queryLength, char *const outResult,
             const int maxResultLength);
 
-    const UnigramProperty getUnigramProperty(const int *const codePoints,
+    const WordProperty getWordProperty(const int *const codePoints,
             const int codePointCount) const;
 
  private:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
index 4322763..6720974 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
@@ -39,11 +39,11 @@
             BufferWithExtendableBuffer::DEFAULT_MAX_ADDITIONAL_BUFFER_SIZE);
     const int extendedRegionSize = headerPolicy->getExtendedRegionSize()
             + mBuffers->getTrieBuffer()->getUsedAdditionalBufferSize();
-    if (!headerPolicy->writeHeaderToBuffer(&headerBuffer, false /* updatesLastUpdatedTime */,
-            false /* updatesLastDecayedTime */, unigramCount, bigramCount, extendedRegionSize)) {
-        AKLOGE("Cannot write header structure to buffer. updatesLastUpdatedTime: %d, "
+    if (!headerPolicy->fillInAndWriteHeaderToBuffer(false /* updatesLastDecayedTime */,
+            unigramCount, bigramCount, extendedRegionSize, &headerBuffer)) {
+        AKLOGE("Cannot write header structure to buffer. "
                 "updatesLastDecayedTime: %d, unigramCount: %d, bigramCount: %d, "
-                "extendedRegionSize: %d", false, false, unigramCount, bigramCount,
+                "extendedRegionSize: %d", false, unigramCount, bigramCount,
                 extendedRegionSize);
         return;
     }
@@ -62,9 +62,8 @@
     }
     BufferWithExtendableBuffer headerBuffer(
             BufferWithExtendableBuffer::DEFAULT_MAX_ADDITIONAL_BUFFER_SIZE);
-    if (!headerPolicy->writeHeaderToBuffer(&headerBuffer, true /* updatesLastUpdatedTime */,
-            true /* updatesLastDecayedTime */, unigramCount, bigramCount,
-            0 /* extendedRegionSize */)) {
+    if (!headerPolicy->fillInAndWriteHeaderToBuffer(true /* updatesLastDecayedTime */,
+            unigramCount, bigramCount, 0 /* extendedRegionSize */, &headerBuffer)) {
         return;
     }
     dictBuffers.get()->flushHeaderAndDictBuffers(dictDirPath, &headerBuffer);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
index 442373b..84403c8 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
@@ -48,9 +48,9 @@
     HeaderPolicy headerPolicy(FormatUtils::VERSION_4, attributeMap);
     Ver4DictBuffers::Ver4DictBuffersPtr dictBuffers =
             Ver4DictBuffers::createVer4DictBuffers(&headerPolicy);
-    headerPolicy.writeHeaderToBuffer(dictBuffers.get()->getWritableHeaderBuffer(),
-            true /* updatesLastUpdatedTime */, true /* updatesLastDecayedTime */,
-            0 /* unigramCount */, 0 /* bigramCount */, 0 /* extendedRegionSize */);
+    headerPolicy.fillInAndWriteHeaderToBuffer(true /* updatesLastDecayedTime */,
+            0 /* unigramCount */, 0 /* bigramCount */,
+            0 /* extendedRegionSize */, dictBuffers.get()->getWritableHeaderBuffer());
     if (!DynamicPtWritingUtils::writeEmptyDictionary(
             dictBuffers.get()->getWritableTrieBuffer(), 0 /* rootPos */)) {
         AKLOGE("Empty ver4 dictionary structure cannot be created on memory.");
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
similarity index 97%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
index cbe2d59..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();
 
@@ -92,8 +92,8 @@
 
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
-        final String resolvedActual = KeySpecParser.resolveTextReference(value, mTextsSet);
-        final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual);
+        final String resolvedActual = mTextsSet.resolveTextReference(value);
+        final String[] actual = MoreKeySpec.splitKeySpecs(resolvedActual);
         final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
         assertArrayEquals(message, expected, actual);
     }
@@ -117,13 +117,11 @@
     private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
 
     public void testResolveNullText() {
-        assertNull("resolve null", KeySpecParser.resolveTextReference(
-                null, mTextsSet));
+        assertNull("resolve null", mTextsSet.resolveTextReference(null));
     }
 
     public void testResolveEmptyText() {
-        assertNull("resolve empty text", KeySpecParser.resolveTextReference(
-                "!text/empty_string", mTextsSet));
+        assertNull("resolve empty text", mTextsSet.resolveTextReference("!text/empty_string"));
     }
 
     public void testSplitZero() {
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 afb2b03..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,9 +71,10 @@
         mSettingsIconId = KeySpecParser.getIconId(ICON_SETTINGS);
     }
 
-    private void assertParser(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
-        final String labelResolved = KeySpecParser.resolveTextReference(moreKeySpec, mTextsSet);
+    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);
         assertEquals(message + " [label]", expectedLabel, spec.mLabel);
@@ -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/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index 1dc1f5a..343ab42 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -21,7 +21,11 @@
 import android.util.Pair;
 
 import com.android.inputmethod.latin.makedict.CodePointUtils;
+import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.utils.FileUtils;
 
 import java.io.File;
@@ -98,9 +102,13 @@
                 getContext().getCacheDir());
         FileUtils.deleteRecursively(file);
         Map<String, String> attributeMap = new HashMap<String, String>();
-        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, dictId);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, dictId);
+        attributeMap.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY,
+                String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
+        attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
-        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_ATTRIBUTE,
+        attributeMap.put(FormatSpec.FileHeader.HAS_HISTORICAL_INFO_KEY,
                 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE);
         if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                 FormatSpec.VERSION4, attributeMap)) {
@@ -119,6 +127,50 @@
         return BinaryDictionary.setCurrentTimeForTest(-1);
     }
 
+    public void testReadDictInJavaSide() {
+        testReadDictInJavaSide(FormatSpec.VERSION4);
+    }
+
+    private void testReadDictInJavaSide(final int formatVersion) {
+        setCurrentTimeForTestMode(mCurrentTime);
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary", formatVersion);
+        } catch (IOException e) {
+            fail("IOException while writing an initial dictionary : " + e);
+        }
+        BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
+                0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
+                Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+        addUnigramWord(binaryDictionary, "a", DUMMY_PROBABILITY);
+        addUnigramWord(binaryDictionary, "ab", DUMMY_PROBABILITY);
+        addUnigramWord(binaryDictionary, "aaa", DUMMY_PROBABILITY);
+        addBigramWords(binaryDictionary, "a", "aaa", DUMMY_PROBABILITY);
+        binaryDictionary.flushWithGC();
+        binaryDictionary.close();
+
+        final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile);
+        try {
+            final FusionDictionary dict = dictDecoder.readDictionaryBinary(null,
+                    false /* deleteDictIfBroken */);
+            PtNode ptNode = FusionDictionary.findWordInTree(dict.mRootNodeArray, "a");
+            assertNotNull(ptNode);
+            assertTrue(ptNode.isTerminal());
+            assertNotNull(ptNode.getBigram("aaa"));
+            ptNode = FusionDictionary.findWordInTree(dict.mRootNodeArray, "ab");
+            assertNotNull(ptNode);
+            assertTrue(ptNode.isTerminal());
+            ptNode = FusionDictionary.findWordInTree(dict.mRootNodeArray, "aaa");
+            assertNotNull(ptNode);
+            assertTrue(ptNode.isTerminal());
+        } catch (IOException e) {
+            fail("IOException while reading dictionary: " + e);
+        } catch (UnsupportedFormatException e) {
+            fail("Unsupported format: " + e);
+        }
+        dictFile.delete();
+    }
+
     public void testControlCurrentTime() {
         testControlCurrentTime(FormatSpec.VERSION4);
     }
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index 73b6fdc..5294bb0 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -26,7 +26,7 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
-import com.android.inputmethod.latin.utils.UnigramProperty;
+import com.android.inputmethod.latin.utils.WordProperty;
 
 import java.io.File;
 import java.io.IOException;
@@ -871,11 +871,11 @@
         }
     }
 
-    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;
@@ -892,9 +892,8 @@
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
 
-        final UnigramProperty invalidUnigramProperty =
-                binaryDictionary.getUnigramProperty("dummyWord");
-        assertFalse(invalidUnigramProperty.isValid());
+        final WordProperty invalidWordProperty = binaryDictionary.getWordProperty("dummyWord");
+        assertFalse(invalidWordProperty.isValid());
 
         for (int i = 0; i < ITERATION_COUNT; i++) {
             final String word = CodePointUtils.generateWord(random, codePointSet);
@@ -905,16 +904,15 @@
             binaryDictionary.addUnigramWord(word, unigramProbability,
                     null /* shortcutTarget */, BinaryDictionary.NOT_A_PROBABILITY,
                     isNotAWord, isBlacklisted, BinaryDictionary.NOT_A_VALID_TIMESTAMP);
-            final UnigramProperty unigramProperty =
-                    binaryDictionary.getUnigramProperty(word);
-            assertEquals(word, unigramProperty.mCodePoints);
-            assertTrue(unigramProperty.isValid());
-            assertEquals(isNotAWord, unigramProperty.mIsNotAWord);
-            assertEquals(isBlacklisted, unigramProperty.mIsBlacklisted);
-            assertEquals(false, unigramProperty.mHasBigrams);
-            assertEquals(false, unigramProperty.mHasShortcuts);
-            assertEquals(unigramProbability, unigramProperty.mProbability);
-            assertTrue(unigramProperty.mShortcutTargets.isEmpty());
+            final WordProperty wordProperty = binaryDictionary.getWordProperty(word);
+            assertEquals(word, wordProperty.mCodePoints);
+            assertTrue(wordProperty.isValid());
+            assertEquals(isNotAWord, wordProperty.mIsNotAWord);
+            assertEquals(isBlacklisted, wordProperty.mIsBlacklisted);
+            assertEquals(false, wordProperty.mHasBigrams);
+            assertEquals(false, wordProperty.mHasShortcuts);
+            assertEquals(unigramProbability, wordProperty.mProbabilityInfo.mProbability);
+            assertTrue(wordProperty.mShortcutTargets.isEmpty());
         }
     }
 
@@ -938,28 +936,28 @@
         binaryDictionary.addUnigramWord("aaa", unigramProbability, "zzz",
                 shortcutProbability, false /* isNotAWord */, false /* isBlacklisted */,
                 0 /* timestamp */);
-        UnigramProperty unigramProperty = binaryDictionary.getUnigramProperty("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.getUnigramProperty("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.getUnigramProperty("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);
@@ -967,9 +965,9 @@
         shortcutTargets.put("zzz", updatedShortcutProbability);
         shortcutTargets.put("yyy", shortcutProbability);
         binaryDictionary.flushWithGC();
-        unigramProperty = binaryDictionary.getUnigramProperty("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);
@@ -1036,14 +1034,15 @@
         }
 
         for (final String word : words) {
-            final UnigramProperty unigramProperty = binaryDictionary.getUnigramProperty(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 b5a71f0..715db2f 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -17,11 +17,11 @@
 package com.android.inputmethod.latin.makedict;
 
 import android.test.AndroidTestCase;
-import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
@@ -76,6 +76,7 @@
 
     public BinaryDictDecoderEncoderTests(final long seed, final int maxUnigrams) {
         super();
+        BinaryDictionary.setCurrentTimeForTest(0);
         Log.e(TAG, "Testing dictionary: seed is " + seed);
         final Random random = new Random(seed);
         sWords.clear();
@@ -107,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);
@@ -262,7 +276,7 @@
                 getContext().getCacheDir());
 
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion));
+                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions));
         addUnigrams(words.size(), dict, words, shortcuts);
         addBigrams(dict, words, bigrams);
         checkDictionary(dict, words, bigrams, shortcuts);
@@ -317,7 +331,6 @@
                 BinaryDictUtils.VERSION4_OPTIONS_WITHOUT_TIMESTAMP);
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER,
                 BinaryDictUtils.VERSION4_OPTIONS_WITH_TIMESTAMP);
-
         for (final String result : results) {
             Log.d(TAG, result);
         }
@@ -344,14 +357,16 @@
             final SparseArray<List<Integer>> expectedBigrams,
             final TreeMap<Integer, String> resultWords,
             final TreeMap<Integer, Integer> resultFrequencies,
-            final TreeMap<Integer, ArrayList<PendingAttribute>> resultBigrams) {
+            final TreeMap<Integer, ArrayList<PendingAttribute>> resultBigrams,
+            final boolean checkProbability) {
         // check unigrams
         final Set<String> actualWordsSet = new HashSet<String>(resultWords.values());
         final Set<String> expectedWordsSet = new HashSet<String>(expectedWords);
         assertEquals(actualWordsSet, expectedWordsSet);
-
-        for (int freq : resultFrequencies.values()) {
-            assertEquals(freq, UNIGRAM_FREQ);
+        if (checkProbability) {
+            for (int freq : resultFrequencies.values()) {
+                assertEquals(freq, UNIGRAM_FREQ);
+            }
         }
 
         // check bigrams
@@ -377,16 +392,19 @@
                 }
                 actBigrams.get(word1).add(word2);
 
-                final int bigramFreq = BinaryDictIOUtils.reconstructBigramFrequency(
-                        unigramFreq, attr.mFrequency);
-                assertTrue(Math.abs(bigramFreq - BIGRAM_FREQ) < TOLERANCE_OF_BIGRAM_FREQ);
+                if (checkProbability) {
+                    final int bigramFreq = BinaryDictIOUtils.reconstructBigramFrequency(
+                            unigramFreq, attr.mFrequency);
+                    assertTrue(Math.abs(bigramFreq - BIGRAM_FREQ) < TOLERANCE_OF_BIGRAM_FREQ);
+                }
             }
         }
         assertEquals(actBigrams, expBigrams);
     }
 
     private long timeAndCheckReadUnigramsAndBigramsBinary(final File file, final List<String> words,
-            final SparseArray<List<Integer>> bigrams, final int bufferType) {
+            final SparseArray<List<Integer>> bigrams, final int bufferType,
+            final boolean checkProbability) {
         final TreeMap<Integer, String> resultWords = CollectionUtils.newTreeMap();
         final TreeMap<Integer, ArrayList<PendingAttribute>> resultBigrams =
                 CollectionUtils.newTreeMap();
@@ -404,7 +422,7 @@
             Log.e(TAG, "UnsupportedFormatException", e);
         }
 
-        checkWordMap(words, bigrams, resultWords, resultFreqs, resultBigrams);
+        checkWordMap(words, bigrams, resultWords, resultFreqs, resultBigrams, checkProbability);
         return diff;
     }
 
@@ -418,13 +436,17 @@
 
         // making the dictionary from lists of words.
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion));
+                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions));
         addUnigrams(words.size(), dict, words, null /* shortcutMap */);
         addBigrams(dict, words, bigrams);
 
         timeWritingDictToFile(file, dict, formatOptions);
 
-        long wordMap = timeAndCheckReadUnigramsAndBigramsBinary(file, words, bigrams, bufferType);
+        // Caveat: Currently, the Java code to read a v4 dictionary doesn't calculate the
+        // probability when there's a timestamp for the entry.
+        // TODO: Abandon the Java code, and implement the v4 dictionary reading code in native.
+        long wordMap = timeAndCheckReadUnigramsAndBigramsBinary(file, words, bigrams, bufferType,
+                !formatOptions.mHasTimestamp /* checkProbability */);
         long fullReading = timeReadingAndCheckDict(file, words, bigrams, null /* shortcutMap */,
                 bufferType);
 
@@ -517,7 +539,7 @@
                 getContext().getCacheDir());
 
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
-                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion));
+                BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions));
         addUnigrams(sWords.size(), dict, sWords, null /* shortcutMap */);
         addBigrams(dict, words, bigrams);
         timeWritingDictToFile(file, dict, formatOptions);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
index f7a808c..20cf9a5 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
@@ -36,11 +36,18 @@
     public static final FormatSpec.FormatOptions VERSION4_OPTIONS_WITH_TIMESTAMP =
             new FormatSpec.FormatOptions(FormatSpec.VERSION4, true /* hasTimestamp */);
 
-    public static DictionaryOptions makeDictionaryOptions(final String id, final String version) {
+    public static DictionaryOptions makeDictionaryOptions(final String id, final String version,
+            final FormatSpec.FormatOptions formatOptions) {
         final DictionaryOptions options = new DictionaryOptions(new HashMap<String, String>());
-        options.mAttributes.put(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, "en_US");
-        options.mAttributes.put(FileHeader.DICTIONARY_ID_ATTRIBUTE, id);
-        options.mAttributes.put(FileHeader.DICTIONARY_VERSION_ATTRIBUTE, version);
+        options.mAttributes.put(FileHeader.DICTIONARY_LOCALE_KEY, "en_US");
+        options.mAttributes.put(FileHeader.DICTIONARY_ID_KEY, id);
+        options.mAttributes.put(FileHeader.DICTIONARY_VERSION_KEY, version);
+        if (formatOptions.mHasTimestamp) {
+            options.mAttributes.put(FileHeader.HAS_HISTORICAL_INFO_KEY,
+                    FileHeader.ATTRIBUTE_VALUE_TRUE);
+            options.mAttributes.put(FileHeader.USES_FORGETTING_CURVE_KEY,
+                    FileHeader.ATTRIBUTE_VALUE_TRUE);
+        }
         return options;
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
index ae08b49..93731b3 100644
--- a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
@@ -56,9 +56,9 @@
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
     private static final HashMap<String, String> HEADER_OPTIONS = new HashMap<String, String>();
     static {
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, "en_US");
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_ID_ATTRIBUTE, "test");
-        HEADER_OPTIONS.put(FileHeader.DICTIONARY_VERSION_ATTRIBUTE, "1000");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_LOCALE_KEY, "en_US");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_ID_KEY, "test");
+        HEADER_OPTIONS.put(FileHeader.DICTIONARY_VERSION_KEY, "1000");
     }
 
     /**
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
index b7b1fe6..adfb920 100644
--- a/tools/dicttool/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -49,7 +49,7 @@
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/LocaleUtils.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/ResizableIntArray.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/StringUtils.java \
-        $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/UnigramProperty.java
+        $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/WordProperty.java
 
 DICTTOOL_ONDEVICE_TESTS_DIRECTORY := \
         $(LATINIME_LOCAL_DIR)/tests/src/com/android/inputmethod/latin/makedict/
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index 0c11f86..d8059e4 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -48,9 +48,9 @@
 
         // Create a thrice-compressed dictionary file.
         final DictionaryOptions testOptions = new DictionaryOptions(new HashMap<String, String>());
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE, VERSION);
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, LOCALE);
-        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, ID);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_VERSION_KEY, VERSION);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY, LOCALE);
+        testOptions.mAttributes.put(FormatSpec.FileHeader.DICTIONARY_ID_KEY, ID);
         final FusionDictionary dict = new FusionDictionary(new PtNodeArray(), testOptions);
         dict.add("foo", TEST_FREQ, null, false /* isNotAWord */);
         dict.add("fta", 1, null, false /* isNotAWord */);
@@ -80,11 +80,11 @@
                 null /* dict : an optional dictionary to add words to, or null */,
                 false /* deleteDictIfBroken */);
         assertEquals("Wrong version attribute", VERSION, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_VERSION_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_VERSION_KEY));
         assertEquals("Wrong locale attribute", LOCALE, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_LOCALE_KEY));
         assertEquals("Wrong id attribute", ID, resultDict.mOptions.mAttributes.get(
-                FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE));
+                FormatSpec.FileHeader.DICTIONARY_ID_KEY));
         assertEquals("Dictionary can't be read back correctly",
                 FusionDictionary.findWordInTree(resultDict.mRootNodeArray, "foo").getFrequency(),
                 TEST_FREQ);
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 a8ac981..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
@@ -18,8 +18,10 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+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;
@@ -45,6 +47,10 @@
  *   KeyboardTextsSet.java
  */
 public final class KeyboardTextsSet {
+    public static final String PREFIX_TEXT = "!text/";
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
+
     // Language to texts map.
     private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
     private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
@@ -87,6 +93,67 @@
         return (text == null) ? LANGUAGE_DEFAULT[id] : text;
     }
 
+    private static int searchTextNameEnd(final String text, final int start) {
+        final int size = text.length();
+        for (int pos = start; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // Label name should be consisted of [a-zA-Z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
+
+    public String resolveTextReference(final String rawText) {
+        if (TextUtils.isEmpty(rawText)) {
+            return null;
+        }
+        int level = 0;
+        String text = rawText;
+        StringBuilder sb;
+        do {
+            level++;
+            if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
+                throw new RuntimeException("too many @string/resource indirection: " + text);
+            }
+
+            final int prefixLen = PREFIX_TEXT.length();
+            final int size = text.length();
+            if (size < prefixLen) {
+                return TextUtils.isEmpty(text) ? null : text;
+            }
+
+            sb = null;
+            for (int pos = 0; pos < size; pos++) {
+                final char c = text.charAt(pos);
+                if (text.startsWith(PREFIX_TEXT, pos)) {
+                    if (sb == null) {
+                        sb = new StringBuilder(text.substring(0, pos));
+                    }
+                    final int end = searchTextNameEnd(text, pos + prefixLen);
+                    final String name = text.substring(pos + prefixLen, end);
+                    sb.append(getText(name));
+                    pos = end - 1;
+                } else if (c == BACKSLASH) {
+                    if (sb != null) {
+                        // Append both escape character and escaped character.
+                        sb.append(text.substring(pos, Math.min(pos + 2, size)));
+                    }
+                    pos++;
+                } else if (sb != null) {
+                    sb.append(c);
+                }
+            }
+
+            if (sb != null) {
+                text = sb.toString();
+            }
+        } while (sb != null);
+        return TextUtils.isEmpty(text) ? null : text;
+    }
+
     // These texts' name should be aligned with the @string/<name> in
     // values*/strings-action-keys.xml.
     private static final String[] RESOURCE_NAMES = {
diff --git a/tools/make-keyboard-text/res/values-fa/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-fa/donottranslate-more-keys.xml
index 5bcfe7f..a053a88 100644
--- a/tools/make-keyboard-text/res/values-fa/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-fa/donottranslate-more-keys.xml
@@ -87,7 +87,6 @@
     <string name="keylabel_for_tablet_comma">"&#x060C;"</string>
     <string name="keyhintlabel_for_tablet_comma">"&#x061F;"</string>
     <string name="more_keys_for_tablet_comma">"!fixedColumnOrder!4,:,!,&#x061F;,&#x061B;,-,/,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;"</string>
-    <string name="keyhintlabel_for_period">"&#x061F;"</string>
     <!-- U+FDFC: "﷼" RIAL SIGN -->
     <string name="keylabel_for_currency">&#xFDFC;</string>
     <!-- U+061F: "؟" ARABIC QUESTION MARK