keep history after reset to jb-ub-latinimegoogle-bayo
diff --git a/dictionaries/en_GB_wordlist.combined.gz b/dictionaries/en_GB_wordlist.combined.gz
index 839f3ef..50647b8 100644
--- a/dictionaries/en_GB_wordlist.combined.gz
+++ b/dictionaries/en_GB_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_US_wordlist.combined.gz b/dictionaries/en_US_wordlist.combined.gz
index 5595c75..19f9ab4 100644
--- a/dictionaries/en_US_wordlist.combined.gz
+++ b/dictionaries/en_US_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_wordlist.combined.gz b/dictionaries/en_wordlist.combined.gz
index 69c39d5..874a5de 100644
--- a/dictionaries/en_wordlist.combined.gz
+++ b/dictionaries/en_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/fr_wordlist.combined.gz b/dictionaries/fr_wordlist.combined.gz
index 1a18320..49dfd79 100644
--- a/dictionaries/fr_wordlist.combined.gz
+++ b/dictionaries/fr_wordlist.combined.gz
Binary files differ
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index b54406f..031d62e 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -110,6 +110,12 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".personalization.DictionaryDecayBroadcastReciever">
+            <intent-filter>
+                <action android:name="com.android.inputmethod.latin.personalization.DICT_DECAY" />
+            </intent-filter>
+        </receiver>
+
         <receiver android:name=".DictionaryPackInstallBroadcastReceiver">
             <intent-filter>
                 <action android:name="com.android.inputmethod.dictionarypack.aosp.UNKNOWN_CLIENT" />
diff --git a/java/res/color/key_text_color_ics.xml b/java/res/color/key_text_color_ics.xml
new file mode 100644
index 0000000..c6f111a
--- /dev/null
+++ b/java/res/color/key_text_color_ics.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Functional keys. -->
+    <item android:state_single="true" android:state_pressed="true"
+          android:color="@color/key_text_color_functional_ics" />
+    <item android:state_single="true"
+          android:color="@color/key_text_color_functional_ics" />
+
+    <!-- Action keys. -->
+    <item android:state_active="true" android:state_pressed="true"
+          android:color="@color/key_text_color_normal_ics" />
+    <item android:state_active="true"
+          android:color="@color/key_text_color_normal_ics" />
+
+    <!-- Toggle keys. Use checkable/checked state. -->
+    <item android:state_checkable="true" android:state_checked="true" android:state_pressed="true"
+          android:color="@color/key_text_color_normal_ics" />
+    <item android:state_checkable="true" android:state_pressed="true"
+          android:color="@color/key_text_color_normal_ics" />
+    <item android:state_checkable="true" android:state_checked="true"
+          android:color="@color/key_text_color_normal_ics" />
+    <item android:state_checkable="true"
+          android:color="@color/key_text_color_normal_ics" />
+
+    <!-- Empty background keys. -->
+    <item android:state_empty="true"
+          android:color="@color/key_text_color_normal_ics" />
+
+    <!-- Normal keys. -->
+    <item android:state_pressed="true"
+          android:color="@color/key_text_color_normal_ics" />
+    <item android:color="@color/key_text_color_normal_ics" />
+</selector>
diff --git a/java/res/drawable-hdpi/sym_keyboard_settings_holo_dark.png b/java/res/drawable-hdpi/sym_keyboard_settings_holo_dark.png
index c76008a..5af09ad 100644
--- a/java/res/drawable-hdpi/sym_keyboard_settings_holo_dark.png
+++ b/java/res/drawable-hdpi/sym_keyboard_settings_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-hdpi/sym_keyboard_smiley_holo_dark.png b/java/res/drawable-hdpi/sym_keyboard_smiley_holo_dark.png
new file mode 100644
index 0000000..cfacbc2
--- /dev/null
+++ b/java/res/drawable-hdpi/sym_keyboard_smiley_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-mdpi/sym_keyboard_settings_holo_dark.png b/java/res/drawable-mdpi/sym_keyboard_settings_holo_dark.png
index a76a976..36c8c96 100644
--- a/java/res/drawable-mdpi/sym_keyboard_settings_holo_dark.png
+++ b/java/res/drawable-mdpi/sym_keyboard_settings_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-mdpi/sym_keyboard_smiley_holo_dark.png b/java/res/drawable-mdpi/sym_keyboard_smiley_holo_dark.png
index 71272bb..067ad54 100644
--- a/java/res/drawable-mdpi/sym_keyboard_smiley_holo_dark.png
+++ b/java/res/drawable-mdpi/sym_keyboard_smiley_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/sym_keyboard_settings_holo_dark.png b/java/res/drawable-xhdpi/sym_keyboard_settings_holo_dark.png
index 05eaffe..99ee97d 100644
--- a/java/res/drawable-xhdpi/sym_keyboard_settings_holo_dark.png
+++ b/java/res/drawable-xhdpi/sym_keyboard_settings_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xhdpi/sym_keyboard_smiley_holo_dark.png b/java/res/drawable-xhdpi/sym_keyboard_smiley_holo_dark.png
index 686831f..e6baa2e 100644
--- a/java/res/drawable-xhdpi/sym_keyboard_smiley_holo_dark.png
+++ b/java/res/drawable-xhdpi/sym_keyboard_smiley_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_settings_holo_dark.png b/java/res/drawable-xxhdpi/sym_keyboard_settings_holo_dark.png
index e435846..7041bb6 100644
--- a/java/res/drawable-xxhdpi/sym_keyboard_settings_holo_dark.png
+++ b/java/res/drawable-xxhdpi/sym_keyboard_settings_holo_dark.png
Binary files differ
diff --git a/java/res/drawable-xxhdpi/sym_keyboard_smiley_holo_dark.png b/java/res/drawable-xxhdpi/sym_keyboard_smiley_holo_dark.png
index 04b7216..5973ac3 100644
--- a/java/res/drawable-xxhdpi/sym_keyboard_smiley_holo_dark.png
+++ b/java/res/drawable-xxhdpi/sym_keyboard_smiley_holo_dark.png
Binary files differ
diff --git a/java/res/layout/emoji_keyboard_tab_icon.xml b/java/res/layout/emoji_keyboard_tab_icon.xml
index d79276e..1609f6a 100644
--- a/java/res/layout/emoji_keyboard_tab_icon.xml
+++ b/java/res/layout/emoji_keyboard_tab_icon.xml
@@ -23,4 +23,5 @@
     android:layout_weight="1.0"
     android:layout_height="wrap_content"
     android:gravity="center"
+    android:scaleType="center"
 />
diff --git a/java/res/layout/emoji_keyboard_view.xml b/java/res/layout/emoji_palettes_view.xml
similarity index 93%
rename from java/res/layout/emoji_keyboard_view.xml
rename to java/res/layout/emoji_palettes_view.xml
index 4566a5a..1c6da90 100644
--- a/java/res/layout/emoji_keyboard_view.xml
+++ b/java/res/layout/emoji_palettes_view.xml
@@ -18,13 +18,13 @@
 */
 -->
 
-<com.android.inputmethod.keyboard.EmojiKeyboardView
+<com.android.inputmethod.keyboard.EmojiPalettesView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/emoji_keyboard_view"
     android:orientation="vertical"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    style="?attr/emojiKeyboardViewStyle"
+    style="?attr/emojiPalettesViewStyle"
 >
     <LinearLayout
         android:orientation="horizontal"
@@ -101,10 +101,10 @@
             android:layout_weight="0.70"
             android:layout_height="match_parent" />
         <ImageButton
-            android:id="@+id/emoji_keyboard_send"
+            android:id="@+id/emoji_keyboard_alphabet2"
             android:layout_width="0dip"
             android:layout_weight="0.15"
             android:layout_height="match_parent"
-            android:src="@drawable/sym_keyboard_return_holo_dark" />
+            android:src="@drawable/ic_ime_switcher_dark" />
     </LinearLayout>
-</com.android.inputmethod.keyboard.EmojiKeyboardView>
+</com.android.inputmethod.keyboard.EmojiPalettesView>
diff --git a/java/res/layout/input_view.xml b/java/res/layout/input_view.xml
index 0b682d1..1e7a384 100644
--- a/java/res/layout/input_view.xml
+++ b/java/res/layout/input_view.xml
@@ -56,5 +56,5 @@
             android:layout_height="wrap_content" />
     </LinearLayout>
     <include
-        layout="@layout/emoji_keyboard_view" />
+        layout="@layout/emoji_palettes_view" />
 </com.android.inputmethod.latin.InputView>
diff --git a/java/res/raw/main_en.dict b/java/res/raw/main_en.dict
index bef6b10..09b6992 100644
--- a/java/res/raw/main_en.dict
+++ b/java/res/raw/main_en.dict
Binary files differ
diff --git a/java/res/raw/main_fr.dict b/java/res/raw/main_fr.dict
index 18f5298..0e5a713 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index f187a73..adf9e5b 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Koppel \'n kopstuk om te hoor hoe wagwoordsleutels hardop gesê word."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Huidige teks is %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Geen teks ingevoer nie"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> korrigeer <xliff:g id="ORIGINAL">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> het outokorreksie"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> korrigeer <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> voer outokorreksie uit"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Sleutelkode %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift aan (tik om te deaktiveer)"</string>
diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml
index 10e791c..9fa68f3 100644
--- a/java/res/values-am/strings.xml
+++ b/java/res/values-am/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"የይለፍቃል ቁልፎች ጮክ በለው ሲነገሩ ለመስማት የጆሮ ማዳመጫ ሰካ::"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"የአሁኑ ፅሁፍ %s ነው"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"ምንም ፅሁፍ አልገባም"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> <xliff:g id="ORIGINAL">%2$s</xliff:g>ን ወደ <xliff:g id="CORRECTED">%3$s</xliff:g> ያርመዋል"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> ራስ-ሰር አርም አለው"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g>ን ወደ <xliff:g id="CORRECTED">%3$s</xliff:g> ያርመዋል"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> ራስ-ሰር እርማትን ያከናውናል"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"የቁልፍ ኮድ%d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"ቀይር"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"ቅያር በርቷል (ለማሰናክል ንካ)"</string>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index 46bff12..1d58832 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"يمكنك توصيل سماعة رأس لسماع مفاتيح كلمة المرور منطوقة بصوت عالٍ."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"النص الحالي هو %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"لم يتم إدخال نص"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> لتصحيح <xliff:g id="ORIGINAL">%2$s</xliff:g> إلى <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> للتصحيح التلقائي"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> لتصحيح <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> إلى <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> للتصحيح التلقائي"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"رمز المفتاح %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"العالي"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift يعمل (انقر للتعطيل)"</string>
diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml
index 9e591de..d9b9708 100644
--- a/java/res/values-be/strings.xml
+++ b/java/res/values-be/strings.xml
@@ -87,9 +87,9 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Каб праслухаць паролi, падключыце гарнiтуру."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Бягучы тэкст %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Тэкст не ўведзены"</string>
-    <!-- no translation found for spoken_auto_correct (5381764628886369268) -->
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
     <skip />
-    <!-- no translation found for spoken_auto_correct_obscured (1186884531440481089) -->
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
     <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Клавішны код %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Зрух"</string>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 0e6c8dd..7064907 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Включете слушалки, за да чуете клавишите за паролата на висок глас."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Текущият текст е %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Няма въведен текст"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"„<xliff:g id="KEY">%1$s</xliff:g>“ коригира „<xliff:g id="ORIGINAL">%2$s</xliff:g>“ на „<xliff:g id="CORRECTED">%3$s</xliff:g>“"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"„<xliff:g id="KEY">%1$s</xliff:g>“ е с автоматично коригиране"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Код на клавишa %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"„Shift“ е включен (докоснете за деактивиране)"</string>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index 043fbd9..ad3c92e 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Connecta un auricular per escoltar les claus de la contrasenya en veu alta."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"El text actual és %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"No s\'ha introduït cap text"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corregeix <xliff:g id="ORIGINAL">%2$s</xliff:g> per <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> té correcció automàtica"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> corregeix <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> per <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> aplica correccions automàtiques"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Clau de codi %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Maj"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Maj activat (pica per desactivar)"</string>
diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml
index 2d5c386..271208d 100644
--- a/java/res/values-cs/strings.xml
+++ b/java/res/values-cs/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Chcete-li slyšet, které klávesy jste při zadávání hesla stiskli, připojte sluchátka."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Aktuální text je %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Není zadán žádný text"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Klávesou <xliff:g id="KEY">%1$s</xliff:g> opravíte <xliff:g id="ORIGINAL">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Klávese <xliff:g id="KEY">%1$s</xliff:g> je přiřazena automatická oprava"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kód klávesy %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Klávesa Shift je zapnutá (vypnete ji klepnutím)."</string>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 0a53691..0439aaf 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Tilslut et headset for at høre indtastningen blive læst højt ved angivelse af adgangskode."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Nuværende tekst er %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Der er ingen indtastet tekst"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> retter <xliff:g id="ORIGINAL">%2$s</xliff:g> til <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> udfører automatisk rettelse"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Tastekode %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift-tast"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Skift er slået til (tryk for at deaktivere)"</string>
diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml
index 010bbd2..28d82ae 100644
--- a/java/res/values-de/strings.xml
+++ b/java/res/values-de/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Schließen Sie ein Headset an, um das Passwort gesprochen zu hören."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Aktueller Text lautet %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Kein Text eingegeben"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Mit <xliff:g id="KEY">%1$s</xliff:g> wird <xliff:g id="ORIGINAL">%2$s</xliff:g> in <xliff:g id="CORRECTED">%3$s</xliff:g> korrigiert."</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Mit <xliff:g id="KEY">%1$s</xliff:g> erfolgt eine Autokorrektur."</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"Mit <xliff:g id="KEY">%1$s</xliff:g> wird <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> in <xliff:g id="CORRECTED">%3$s</xliff:g> korrigiert."</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"Mit <xliff:g id="KEY">%1$s</xliff:g> erfolgt eine Autokorrektur."</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Tastencode %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Umschalttaste"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Umschalttaste aktiviert (zum Deaktivieren berühren)"</string>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index 8b6d0d1..54b3c3e 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Συνδέστε ένα σετ ακουστικών για να ακούσετε τα πλήκτρα του κωδικού πρόσβασης να εκφωνούνται δυνατά."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Το τρέχον κείμενο είναι %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Δεν υπάρχει κείμενο"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Το πλήκτρο <xliff:g id="KEY">%1$s</xliff:g> διορθώνει το στοιχείο <xliff:g id="ORIGINAL">%2$s</xliff:g> σε <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Το πλήκτρο <xliff:g id="KEY">%1$s</xliff:g> διαθέτει αυτόματη διόρθωση"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> διορθώνει το <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> σε <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> εκτελεί αυτόματη διόρθωση"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Κωδικός πλήκτρου %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Το Shift είναι ενεργοποιημένο (πατήστε για απενεργοποίηση)"</string>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 1891a3f..bdffb94 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Plug in a headset to hear password keys spoken aloud."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Current text is %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"No text entered"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corrects <xliff:g id="ORIGINAL">%2$s</xliff:g> to <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> has auto-correction"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> corrects <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> to <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> performs auto-correction"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Key code %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift on (tap to disable)"</string>
diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml
index 1891a3f..bdffb94 100644
--- a/java/res/values-en-rIN/strings.xml
+++ b/java/res/values-en-rIN/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Plug in a headset to hear password keys spoken aloud."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Current text is %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"No text entered"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corrects <xliff:g id="ORIGINAL">%2$s</xliff:g> to <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> has auto-correction"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> corrects <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> to <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> performs auto-correction"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Key code %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift on (tap to disable)"</string>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 331eb38..2d7872a 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Enchufa tus auriculares para escuchar en voz alta qué teclas presionas al ingresar una contraseña."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"El texto actual es %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"No se ingresó texto."</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL">%2$s</xliff:g> por <xliff:g id="CORRECTED">%3$s</xliff:g>."</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige automáticamente."</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> por <xliff:g id="CORRECTED">%3$s</xliff:g>."</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige automáticamente."</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Clave de código %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Mayús"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Se activó el modo Mayúscula (toca para desactivarlo)."</string>
diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml
index 73cc978..4dfc57f 100644
--- a/java/res/values-es/strings.xml
+++ b/java/res/values-es/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Conecta un auricular para escuchar las contraseñas en voz alta."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"El texto actual es %s."</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"No se ha introducido texto."</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL">%2$s</xliff:g> a <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige automáticamente"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> a <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"La tecla <xliff:g id="KEY">%1$s</xliff:g> corrige automáticamente"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Código del teclado: %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Mayús"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Mayúsculas activadas (tocar para inhabilitar)"</string>
diff --git a/java/res/values-et-rEE/strings.xml b/java/res/values-et-rEE/strings.xml
index c7e6fe9..3a27a88 100644
--- a/java/res/values-et-rEE/strings.xml
+++ b/java/res/values-et-rEE/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Ühendage peakomplekt, et kuulata paroole."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Praegune tekst on %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Teksti ei ole sisestatud"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> parandab valiku <xliff:g id="ORIGINAL">%2$s</xliff:g> valikuks <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Klahv <xliff:g id="KEY">%1$s</xliff:g> rakendab automaatse paranduse"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"Klahvi <xliff:g id="KEY">%1$s</xliff:g> vajutamisel parandatakse sõna <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> sõnaks <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"Klahvi <xliff:g id="KEY">%1$s</xliff:g> vajutamisel tehakse automaatne parandus"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Klahvi kood: %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Tõstuklahv"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Tõstuklahv sees (puudutage keelamiseks)"</string>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index 81a3314..a6d9bba 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -86,8 +86,8 @@
     <!-- no translation found for spoken_current_text_is (2485723011272583845) -->
     <skip />
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"متنی وارد نشده است"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g>، ‏<xliff:g id="ORIGINAL">%2$s</xliff:g> را به <xliff:g id="CORRECTED">%3$s</xliff:g> تصحیح می‌کند"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> دارای تصحیح خودکار استj"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g>، ‏<xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> را به <xliff:g id="CORRECTED">%3$s</xliff:g> تصحیح می‌کند"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> تصحیح خودکار را انجام می‌دهد"</string>
     <!-- String.format failed for translation -->
     <!-- no translation found for spoken_description_unknown (3197434010402179157) -->
     <skip />
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index 8f2caab..9f098a1 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Liitä kuulokkeet, niin kuulet mitä näppäimiä painat kirjoittaessasi salasanaa."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Nykyinen teksti on %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Ei kirjoitettua tekstiä"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> korjaa kohteen <xliff:g id="ORIGINAL">%2$s</xliff:g> kohteeksi <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> suorittaa automaattisen korjauksen"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Näppäimen koodi %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Vaihto"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Vaihto päällä (poista käytöstä napauttamalla)"</string>
diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml
index fba0298..0d0c082 100644
--- a/java/res/values-fr-rCA/strings.xml
+++ b/java/res/values-fr-rCA/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Branchez des écouteurs pour entendre l\'énoncé à haute voix des touches lors de la saisie du mot de passe."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Le texte actuel est %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Aucun texte saisi"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL">%2$s</xliff:g> en <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> dispose de la fonctionnalité de correction automatique"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"La touche <xliff:g id="KEY">%1$s</xliff:g> permet de corriger« <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> » par « <xliff:g id="CORRECTED">%3$s</xliff:g> »"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"La touche <xliff:g id="KEY">%1$s</xliff:g> permet d\'activer la correction automatique"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Code touche %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Maj"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Touche Maj activée (appuyer pour désactiver)"</string>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index 9d6c8f4..0f92379 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Branchez des écouteurs pour entendre l\'énoncé à haute voix des touches lors de la saisie du mot de passe."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Le texte actuel est %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Aucun texte saisi"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"La touche <xliff:g id="KEY">%1$s</xliff:g> permet de remplacer \"<xliff:g id="ORIGINAL">%2$s</xliff:g>\" par \"<xliff:g id="CORRECTED">%3$s</xliff:g>\"."</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"La touche <xliff:g id="KEY">%1$s</xliff:g> permet d\'activer la correction automatique."</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Code touche %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Maj"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Touche Maj activée (appuyer pour désactiver)"</string>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index 0e97296..c013e5f 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"ज़ोर से बोली गई पासवर्ड कुंजियां सुनने के लिए हेडसेट प्‍लग इन करें."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"वर्तमान पाठ %s है"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"कोई पाठ दर्ज नहीं किया गया"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g>, <xliff:g id="ORIGINAL">%2$s</xliff:g> को सुधारकर <xliff:g id="CORRECTED">%3$s</xliff:g> बना देती है"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> से स्‍वत: सुधार होगा"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"कुंजी कोड %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"शिफ़्ट"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift चालू (अक्षम करने के लिए टैप करें)"</string>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index f5c9ad5..1928fb9 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Priključite slušalice da biste čuli tipke zaporke izgovorene naglas."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Trenutačni tekst je %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nije unesen tekst"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> ispravlja <xliff:g id="ORIGINAL">%2$s</xliff:g> u <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> ima samoispravljanje"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kôd tipke %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Uključena tipka Shift (dotaknite da onemogućite)"</string>
diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml
index 5f4a181..1a07772 100644
--- a/java/res/values-hu/strings.xml
+++ b/java/res/values-hu/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Csatlakoztasson egy headsetet, ha hallani szeretné a jelszót felolvasva."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"A jelenlegi szöveg: %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Szöveg nincs megadva"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> gomb: a(z) <xliff:g id="ORIGINAL">%2$s</xliff:g> értéket <xliff:g id="CORRECTED">%3$s</xliff:g> értékre javítja"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Automatikus javítás van beállítva a következőhöz: <xliff:g id="KEY">%1$s</xliff:g>"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> billentyű: <xliff:g id="CORRECTED">%3$s</xliff:g> szóra javítja a következőt: <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> billentyű: automatikus javítás"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Billentyűkód: %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift be van kapcsolva (érintse meg a kikapcsoláshoz)"</string>
diff --git a/java/res/values-hy-rAM/strings.xml b/java/res/values-hy-rAM/strings.xml
index 57c0e61..f584cfa 100644
--- a/java/res/values-hy-rAM/strings.xml
+++ b/java/res/values-hy-rAM/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Միացրեք ականջակալը՝ բարձրաձայն արտասանվող գաղտնաբառը լսելու համար:"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Տվյալ տեքստը %s է"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Տեքստ չի մուտքագրվել"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g>-ը շտկում է <xliff:g id="ORIGINAL">%2$s</xliff:g>-ը և դարձնում <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g>-ն ունի ինքնուրույն շտկում"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Բանալու կոդը՝ %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift-ը միացված է (հպել անջատելու համար)"</string>
diff --git a/java/res/values-hy/donottranslate.xml b/java/res/values-hy/donottranslate.xml
index 4a6d188..7b0c566 100644
--- a/java/res/values-hy/donottranslate.xml
+++ b/java/res/values-hy/donottranslate.xml
@@ -26,4 +26,7 @@
     <!-- Symbols that separate words. Adding armenian period and comma. -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"&#x0589;&#x055D;</string>
+    <!-- The sentence separator code point, for capitalization -->
+    <!-- U+0589: "։" ARMENIAN FULL STOP   ; 589h = 1417d -->
+    <integer name="sentence_separator">1417</integer>
 </resources>
diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml
index acd8394..db21673 100644
--- a/java/res/values-in/strings.xml
+++ b/java/res/values-in/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Pasang headset untuk mendengar tombol sandi yang diucapkan dengan keras."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Teks saat ini adalah %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Tidak ada teks yang dimasukkan"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> mengoreksi <xliff:g id="ORIGINAL">%2$s</xliff:g> menjadi <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> memiliki koreksi otomatis"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kode tombol %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift hidup (ketuk untuk mematikan)"</string>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index 830975f..ec27fd5 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Collega gli auricolari per ascoltare la pronuncia dei tasti premuti per la password."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Il testo attuale è %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nessun testo inserito"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corregge <xliff:g id="ORIGINAL">%2$s</xliff:g> con <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> ha la funzione di correzione automatica"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> corregge <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> con <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> esegue correzione automatica"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Codice tasto %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Maiuscolo"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Maiuscolo attivo (tocca per disattivare)"</string>
diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml
index 1ad2c3e..81d2e40 100644
--- a/java/res/values-iw/strings.xml
+++ b/java/res/values-iw/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"חבר אוזניות כדי לשמוע הקראה של מפתחות סיסמה."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"הטקסט הנוכחי הוא %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"לא הוזן טקסט"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> מתקן את <xliff:g id="ORIGINAL">%2$s</xliff:g> ל-<xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> מבצע תיקון אוטומטי"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> מתקן את <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> ל-<xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> מבצע תיקון אוטומטי"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"קוד מקש %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift פועל (הקש כדי להשבית)"</string>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 9cda30b..7e66a7b 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"パスワードのキーが音声出力されるのでヘッドセットを接続してください。"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"現在のテキスト:%s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"テキストが入力されていません"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g>は<xliff:g id="ORIGINAL">%2$s</xliff:g>を<xliff:g id="CORRECTED">%3$s</xliff:g>に修正します"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g>で自動修正が実行されます"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"キーコード:%d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift有効(タップして解除)"</string>
diff --git a/java/res/values-ka-rGE/strings.xml b/java/res/values-ka-rGE/strings.xml
index 9e46c4d..b5da8ba 100644
--- a/java/res/values-ka-rGE/strings.xml
+++ b/java/res/values-ka-rGE/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"შეაერთეთ ყურსაცვამი, რათა მოისმინოთ აკრეფილი პაროლის კლავიშების სახელები."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"მიმდინარე ტექსტი არის %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"ტექსტი არ შეყვანილა"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> შეასწორებს <xliff:g id="ORIGINAL">%2$s</xliff:g>-ს <xliff:g id="CORRECTED">%3$s</xliff:g>-ად"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g>-ს ავტოკორექცია აქვს"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"კლავიატურის კოდი %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift ჩართულია (შეეხეთ გამოსართავად)"</string>
diff --git a/java/res/values-km-rKH/strings.xml b/java/res/values-km-rKH/strings.xml
index 34c4269..150ec02 100644
--- a/java/res/values-km-rKH/strings.xml
+++ b/java/res/values-km-rKH/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"ដោត​កាស ដើម្បី​ស្ដាប់​ពាក្យ​សម្ងាត់។"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"អត្ថបទ​បច្ចុប្បន្ន​គឺ %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"គ្មាន​អត្ថបទ​​​បាន​បញ្ចូល"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> កែ <xliff:g id="ORIGINAL">%2$s</xliff:g> ទៅ <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> មាន​ការ​កែ​ស្វ័យ​ប្រវត្តិ"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"កូដ​គ្រាប់​ចុច %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"បើក Shift (​ប៉ះ​ដើម្បី​បិទ)"</string>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index 630ae7e..2253ce3 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"비밀번호 키를 음성으로 들으려면 헤드셋을 연결하세요."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"입력한 텍스트: %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"입력한 텍스트 없음"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g>을(를) 누르면 <xliff:g id="ORIGINAL">%2$s</xliff:g>을(를) <xliff:g id="CORRECTED">%3$s</xliff:g>(으)로 수정합니다."</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g>을(를) 누르면 자동 수정됩니다."</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"키 코드 %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"시프트 키"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift 사용(사용하지 않으려면 탭하세요.)"</string>
diff --git a/java/res/values-lo-rLA/strings.xml b/java/res/values-lo-rLA/strings.xml
index 0d7f7a2..1597f92 100644
--- a/java/res/values-lo-rLA/strings.xml
+++ b/java/res/values-lo-rLA/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"ສຽບສາຍຫູຟັງເພື່ອຟັງລະຫັດຜ່ານ."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"ຂໍ້ຄວາມປະຈຸບັນແມ່ນ %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"ບໍ່ມີການໃສ່ຂໍ້ຄວາມ"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> ຖືກແປງຈາກ <xliff:g id="ORIGINAL">%2$s</xliff:g> ເປັນ <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> ບໍ່ມີການກວດຄຳຖືກອັດຕະໂນມັດ"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> ແກ້ໄຂ <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> ເປັນ <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> ປະຕິບັດການແປງຄຳຜິດອັດຕະໂນມັດ"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"ລະຫັດກະແຈ %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift ເປີດນຳໃຊ້ຢູ່ (ກົດເພື່ອປິດນຳໃຊ້)"</string>
diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml
index d40b54b..1cb84d0 100644
--- a/java/res/values-lt/strings.xml
+++ b/java/res/values-lt/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Prijunkite ausines, kad išgirstumėte sakomus slaptažodžio klavišus."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Dabartinis tekstas yra %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nėra įvesto teksto"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"„<xliff:g id="KEY">%1$s</xliff:g>“ pataiso „<xliff:g id="ORIGINAL">%2$s</xliff:g>“ į „<xliff:g id="CORRECTED">%3$s</xliff:g>“"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"„<xliff:g id="KEY">%1$s</xliff:g>“ atlieka automatinį taisymą"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> pataiso „<xliff:g id="ORIGINAL_WORD">%2$s</xliff:g>“ į „<xliff:g id="CORRECTED">%3$s</xliff:g>“"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> atlieka automatinį taisymą"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Klavišo kodas %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Antrojo lygio klavišas"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Įjungtas antrasis lygis (palieskite, kad išjungtumėte)"</string>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index cde9c34..221cc2e 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Pievienojiet austiņas, lai dzirdētu paroles rakstzīmes."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Pašreizējais teksts ir %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nav ievadīts teksts"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Nospiežot taustiņu <xliff:g id="KEY">%1$s</xliff:g>, “<xliff:g id="ORIGINAL">%2$s</xliff:g>” tiek labots uz “<xliff:g id="CORRECTED">%3$s</xliff:g>”."</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Taustiņam <xliff:g id="KEY">%1$s</xliff:g> ir automātiskas labošanas funkcija."</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"Nospiežot taustiņu <xliff:g id="KEY">%1$s</xliff:g>, “<xliff:g id="ORIGINAL_WORD">%2$s</xliff:g>” tiek labots uz “<xliff:g id="CORRECTED">%3$s</xliff:g>”."</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"Taustiņam <xliff:g id="KEY">%1$s</xliff:g> ir automātiskas labošanas funkcija."</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Taustiņu kods %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Pārslēgšanas taustiņš"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Pārslēgšanas taustiņš iespējots (pieskarieties, lai atspējotu)"</string>
diff --git a/java/res/values-mn-rMN/strings.xml b/java/res/values-mn-rMN/strings.xml
index 177b533..05ac4ac 100644
--- a/java/res/values-mn-rMN/strings.xml
+++ b/java/res/values-mn-rMN/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Нууц үгний товчнуудыг чангаар уншихыг сонсохын тулд чихэвчээ залгана уу."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Одоогийн текст %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Текст оруулаагүй"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> нь <xliff:g id="ORIGINAL">%2$s</xliff:g>-г <xliff:g id="CORRECTED">%3$s</xliff:g> болгож залруулна"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> автомат залруулагчтай"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Товчийн код %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Сэлгэх"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Сэлгэхийг идэвхжүүлсэн (товшиж идэвхгүйжүүлнэ үү)"</string>
diff --git a/java/res/values-ms-rMY/strings.xml b/java/res/values-ms-rMY/strings.xml
index 0e8b4eb..d59791a 100644
--- a/java/res/values-ms-rMY/strings.xml
+++ b/java/res/values-ms-rMY/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Pasangkan set kepala untuk mendengar kekunci kata laluan disebut dengan kuat."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Teks semasa adalah %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Tiada teks dimasukkan"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> membetulkan <xliff:g id="ORIGINAL">%2$s</xliff:g> menjadi <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> mempunyai auto pembetulan"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kod kunci %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Kunci anjak dihidupkan (ketik untuk melumpuhkan)"</string>
diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml
index 1bd91b6..0a2def8 100644
--- a/java/res/values-nb/strings.xml
+++ b/java/res/values-nb/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Koble til hodetelefoner for å høre opplesing av bokstavene i passordet."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Gjeldende tekst er %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Ingen tekst er skrevet inn"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> korrigerer <xliff:g id="ORIGINAL">%2$s</xliff:g> til <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> har automatisk korrigering"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> retter <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> til <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> utfører automatisk retting"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Tastaturkode %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift er på (trykk for å deaktivere)"</string>
diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml
index b1d1bb3..60db9f0 100644
--- a/java/res/values-nl/strings.xml
+++ b/java/res/values-nl/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Sluit een headset aan om wachtwoordtoetsen hardop te laten voorlezen."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Huidige tekst is %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Geen tekst ingevoerd"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Met <xliff:g id="KEY">%1$s</xliff:g> wordt <xliff:g id="ORIGINAL">%2$s</xliff:g> gecorrigeerd naar <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Met <xliff:g id="KEY">%1$s</xliff:g> voert u automatische correctie uit"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"Met <xliff:g id="KEY">%1$s</xliff:g> wordt <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> gecorrigeerd naar <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"Met <xliff:g id="KEY">%1$s</xliff:g> voert u automatische correctie uit"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Toetscode %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift aan (tik om uit te schakelen)"</string>
diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml
index f830b37..c3ed2d8 100644
--- a/java/res/values-pl/strings.xml
+++ b/java/res/values-pl/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Podłącz zestaw słuchawkowy, aby usłyszeć znaki hasła wypowiadane na głos."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Aktualny tekst: %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nie wprowadzono tekstu"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> poprawia <xliff:g id="ORIGINAL">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> zapewnia autokorektę"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> poprawia <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> wykonuje autokorektę"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kod klawisza: %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift włączony (kliknij, by wyłączyć)"</string>
diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml
index dbf34f9..2a4c2b8 100644
--- a/java/res/values-pt-rPT/strings.xml
+++ b/java/res/values-pt-rPT/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Ligar auscultadores com microfone integrado para ouvir as teclas da palavra-passe."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"O texto atual é %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nenhum texto digitado"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL">%2$s</xliff:g> para <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> tem correção automática"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> para <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> executa correção automática"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Código da tecla %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift ativado (tocar para desativar)"</string>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index 3f98372..8cc0718 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Conecte um fone de ouvido para ouvir as chaves de senha em voz alta."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"O texto atual é %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nenhum texto digitado"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corrige <xliff:g id="ORIGINAL">%2$s</xliff:g> para <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> possui correção automática"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Código de tecla %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift ativado (toque para desativar)"</string>
diff --git a/java/res/values-rm/strings.xml b/java/res/values-rm/strings.xml
index c68d30c..b956619 100644
--- a/java/res/values-rm/strings.xml
+++ b/java/res/values-rm/strings.xml
@@ -139,9 +139,9 @@
     <skip />
     <!-- no translation found for spoken_no_text_entered (7479685225597344496) -->
     <skip />
-    <!-- no translation found for spoken_auto_correct (5381764628886369268) -->
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
     <skip />
-    <!-- no translation found for spoken_auto_correct_obscured (1186884531440481089) -->
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
     <skip />
     <!-- no translation found for spoken_description_unknown (3197434010402179157) -->
     <skip />
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index f67f58e..d8b32e7 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Conectaţi un set căşti-microfon pentru a auzi tastele apăsate când introduceţi parola."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Textul curent este %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nu a fost introdus text"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> corectează <xliff:g id="ORIGINAL">%2$s</xliff:g> cu <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> dispune de corectare automată"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Tasta cu codul %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Tasta Shift este activată (apăsaţi pentru a o dezactiva)"</string>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index 8a5b747..d27207c 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Подключите гарнитуру, чтобы услышать пароль."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Введенный текст: %s."</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Текст не введен"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"При нажатии клавиши \"<xliff:g id="KEY">%1$s</xliff:g>\" слово \"<xliff:g id="ORIGINAL">%2$s</xliff:g>\" будет исправлено на \"<xliff:g id="CORRECTED">%3$s</xliff:g>\""</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Для клавиши \"<xliff:g id="KEY">%1$s</xliff:g>\" назначена функция автоисправления"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Код клавиши:%d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Клавиша верхнего регистра"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Верхний регистр включен (нажмите, чтобы отключить)"</string>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 3f6706c..4ca07c9 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Ak si chcete pri zadávaní hesla vypočuť nahlas vyslovené klávesy, pripojte náhlavnú súpravu."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Aktuálny text je %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Nie je zadaný žiadny text"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Klávesom <xliff:g id="KEY">%1$s</xliff:g> opravíte <xliff:g id="ORIGINAL">%2$s</xliff:g> na <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Klávesom <xliff:g id="KEY">%1$s</xliff:g> spustíte automatické opravy"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Kód klávesu %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Kláves Shift je zapnutý (zakážete ho klepnutím)"</string>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index 6c8115e..34bbe91 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Priključite slušalke, če želite slišati izgovorjene tipke gesla."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Trenutno besedilo je %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Ni vnesenega besedila"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Tipka <xliff:g id="KEY">%1$s</xliff:g> popravi <xliff:g id="ORIGINAL">%2$s</xliff:g> v <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Tipka <xliff:g id="KEY">%1$s</xliff:g> izvede samodejno popravljanje"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Koda tipke %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift je vklopljen (dotaknite se, da onemogočite)"</string>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index 92e465c..4d5df50 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Укључите слушалице да бисте чули наглас изговорене тастере за лозинку."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Тренутни текст је %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Текст није унет"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> исправља <xliff:g id="ORIGINAL">%2$s</xliff:g> у <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> има функцију аутоматског исправљања"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Кôд тастера %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift је укључен (додирните да бисте га онемогућили)"</string>
diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml
index 009e829..84ee1d8 100644
--- a/java/res/values-sv/strings.xml
+++ b/java/res/values-sv/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Anslut hörlurar om du vill att lösenordet ska läsas upp."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Nuvarande text är %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Ingen text har angetts"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> ändrar <xliff:g id="ORIGINAL">%2$s</xliff:g> till <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"Automatisk korrigering används för <xliff:g id="KEY">%1$s</xliff:g>"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Nyckelkod %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Skift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Skift på (knacka lätt för att inaktivera)"</string>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index 984b6cb..2f8f85e 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Chomeka plagi ya kifaa cha kichwa cha kusikiza ili kusikiliza msimbo wa nenosiri inayozungumwa kwa sauti ya juu."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Maandishi ya sasa ni %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Hakuna maandishi yaliyoingizwa"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> hurekebisha <xliff:g id="ORIGINAL">%2$s</xliff:g> kuwa <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> ina urekebishaji wa kiotomatiki"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> hurekebisha <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> kuwa <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> hutekeleza urekebishaji wa kiotomatiki"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Msimbo wa kitufe %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Badilisha"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift imewashwa (gonga ili kulemaza)"</string>
diff --git a/java/res/values-sw600dp-land/dimens.xml b/java/res/values-sw600dp-land/dimens.xml
index d067265..d79e8ca 100644
--- a/java/res/values-sw600dp-land/dimens.xml
+++ b/java/res/values-sw600dp-land/dimens.xml
@@ -66,7 +66,7 @@
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">10%p</fraction>
     <fraction name="emoji_keyboard_row_height">33%p</fraction>
-    <fraction name="emoji_keyboard_key_letter_size">85%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">70%p</fraction>
     <integer name="emoji_keyboard_max_key_count">30</integer>
 
 </resources>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index 591355d..b2f4ae0 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -92,7 +92,7 @@
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">12.5%p</fraction>
     <fraction name="emoji_keyboard_row_height">33%p</fraction>
-    <fraction name="emoji_keyboard_key_letter_size">76%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">60%p</fraction>
     <integer name="emoji_keyboard_max_key_count">24</integer>
 
 </resources>
diff --git a/java/res/values-sw768dp-land/dimens.xml b/java/res/values-sw768dp-land/dimens.xml
index 664630b..ce315b0 100644
--- a/java/res/values-sw768dp-land/dimens.xml
+++ b/java/res/values-sw768dp-land/dimens.xml
@@ -67,7 +67,7 @@
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">7.69%p</fraction>
     <fraction name="emoji_keyboard_row_height">33%p</fraction>
-    <fraction name="emoji_keyboard_key_letter_size">68%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">60%p</fraction>
     <integer name="emoji_keyboard_max_key_count">39</integer>
 
 </resources>
diff --git a/java/res/values-sw768dp/dimens.xml b/java/res/values-sw768dp/dimens.xml
index 1fd933a..c90da7f 100644
--- a/java/res/values-sw768dp/dimens.xml
+++ b/java/res/values-sw768dp/dimens.xml
@@ -92,7 +92,7 @@
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">10%p</fraction>
     <fraction name="emoji_keyboard_row_height">33%p</fraction>
-    <fraction name="emoji_keyboard_key_letter_size">76%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">68%p</fraction>
     <integer name="emoji_keyboard_max_key_count">30</integer>
 
 </resources>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index f2e252d..06fa120 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"เสียบชุดหูฟังเพื่อฟังเสียงเมื่อพิมพ์รหัสผ่าน"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"ข้อความปัจจุบันคือ %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"ไม่มีข้อความ"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> จะแก้ไข <xliff:g id="ORIGINAL">%2$s</xliff:g> เป็น <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> จะมีการแก้ไขอัตโนมัติ"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"รหัสคีย์ %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift เปิดอยู่ (แตะเพื่อปิดใช้งาน)"</string>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index 9a35545..f914958 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Mag-plug in ng headset upang marinig ang mga password key na binabanggit nang malakas."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Ang kasalukuyang teksto ay %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Walang tekstong inilagay"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"Itatama ng pagpindot sa <xliff:g id="KEY">%1$s</xliff:g> ang <xliff:g id="ORIGINAL">%2$s</xliff:g> at gagawing <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"May awtomatikong pagwasto ang <xliff:g id="KEY">%1$s</xliff:g>"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Code ng key %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Naka-on ang shift (i-tap upang huwag paganahin)"</string>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index ab376e1..bd20e56 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Şifre tuşlarının sesli okunmasını dinlemek için mikrofonlu kulaklık takın."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Mevcut metin: %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Hiç metin girilmedi"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> tuşuna basıldığında <xliff:g id="ORIGINAL">%2$s</xliff:g>, <xliff:g id="CORRECTED">%3$s</xliff:g> olarak düzeltilir"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> tuşunda otomatik düzeltme işlevi var"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Tuş kodu: %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Üst Karakter"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Üst karakter açık (devre dışı bırakmak için hafifçe vurun)"</string>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index 2c51652..2a8b937 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Підключіть гарнітуру, щоб прослухати відтворені вголос символи пароля."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Поточний текст – %s."</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Текст не введено"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> виправляє <xliff:g id="ORIGINAL">%2$s</xliff:g> на <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> має функцію автоматичного виправлення"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"<xliff:g id="KEY">%1$s</xliff:g> виправляє <xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> на <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"<xliff:g id="KEY">%1$s</xliff:g> здійснює автоматичне виправлення"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Код клавіші – %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Клавіша Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift увімкнено (швидко торкніться, щоб вимкнути)"</string>
diff --git a/java/res/values-v19/emoji-categories.xml b/java/res/values-v19/emoji-categories.xml
index 658bbfa..51aad6e 100644
--- a/java/res/values-v19/emoji-categories.xml
+++ b/java/res/values-v19/emoji-categories.xml
@@ -204,18 +204,18 @@
         name="emoji_symbols"
         format="string"
     >
-        <item>fe82e|0031,20e3</item>
-        <item>fe82f|0032,20e3</item>
-        <item>fe830|0033,20e3</item>
-        <item>fe831|0034,20e3</item>
-        <item>fe832|0035,20e3</item>
-        <item>fe833|0036,20e3</item>
-        <item>fe834|0037,20e3</item>
-        <item>fe835|0038,20e3</item>
-        <item>fe836|0039,20e3</item>
-        <item>fe837|0030,20e3</item>
-        <item>1f51f</item>
-        <item>fe82c|0023,20e3</item>
+        <item>fe82e|0031,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe82f|0032,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe830|0033,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe831|0034,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe832|0035,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe833|0036,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe834|0037,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe835|0038,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe836|0039,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe837|0030,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>1f51f||99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe82c|0023,20e3|99</item> <!-- TODO: fix support min sdk version (99) -->
         <item>1f51d</item>
         <item>1f519</item>
         <item>1f51b</item>
@@ -875,16 +875,16 @@
         <item>1f48e</item>
         <item>1f490</item>
         <item>1f492</item>
-        <item>fe4e5|1f1ef,1f1f5</item>
-        <item>fe4e6|1f1fa,1f1f8</item>
-        <item>fe4e7|1f1eb,1f1f7</item>
-        <item>fe4e8|1f1e9,1f1ea</item>
-        <item>fe4e9|1f1ee,1f1f9</item>
-        <item>fe4ea|1f1ec,1f1e7</item>
-        <item>fe4eb|1f1ea,1f1f8</item>
-        <item>fe4ec|1f1f7,1f1fa</item>
-        <item>fe4ed|1f1e8,1f1f3</item>
-        <item>fe4ee|1f1f0,1f1f7</item>
+        <item>fe4e5|1f1ef,1f1f5|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4e6|1f1fa,1f1f8|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4e7|1f1eb,1f1f7|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4e8|1f1e9,1f1ea|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4e9|1f1ee,1f1f9|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4ea|1f1ec,1f1e7|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4eb|1f1ea,1f1f8|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4ec|1f1f7,1f1fa|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4ed|1f1e8,1f1f3|99</item> <!-- TODO: fix support min sdk version (99) -->
+        <item>fe4ee|1f1f0,1f1f7|99</item> <!-- TODO: fix support min sdk version (99) -->
     </array>
     <array
         name="emoji_emoticons"
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index 64b804a..c103e96 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Cắm tai nghe để nghe mật khẩu."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Ký tự hiện tại là %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Không có ký tự nào được nhập"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"<xliff:g id="KEY">%1$s</xliff:g> sửa <xliff:g id="ORIGINAL">%2$s</xliff:g> thành <xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"<xliff:g id="KEY">%1$s</xliff:g> có tính năng tự động sửa"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Mã phím %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift đang bật (bấm để tắt)"</string>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index 7683c84..6df35f1 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"需要插入耳机才能听到密码的按键声。"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"当前文本为%s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"未输入文字"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"按<xliff:g id="KEY">%1$s</xliff:g>可将<xliff:g id="ORIGINAL">%2$s</xliff:g>更正为<xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"按<xliff:g id="KEY">%1$s</xliff:g>可执行自动更正"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"键码为 %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift 模式已启用(点按即可停用)"</string>
diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml
index 0090ac3..194136f 100644
--- a/java/res/values-zh-rHK/strings.xml
+++ b/java/res/values-zh-rHK/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"插上耳機即可聽到系統朗讀密碼鍵。"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"目前文字為 %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"未輸入文字"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"按下「<xliff:g id="KEY">%1$s</xliff:g>」可將「<xliff:g id="ORIGINAL">%2$s</xliff:g>」修正為「<xliff:g id="CORRECTED">%3$s</xliff:g>」"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"「<xliff:g id="KEY">%1$s</xliff:g>」鍵具自動修正功能"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"按鍵代碼 %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift 鍵"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift 鍵已開啟 (輕按即可停用)"</string>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index ef3c833..e9853e2 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -84,8 +84,10 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"連接耳機即可聽取系統朗讀密碼按鍵。"</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"目前文字為 %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"未輸入文字"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"按下「<xliff:g id="KEY">%1$s</xliff:g>」可將「<xliff:g id="ORIGINAL">%2$s</xliff:g>」修正為「<xliff:g id="CORRECTED">%3$s</xliff:g>」"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"按下「<xliff:g id="KEY">%1$s</xliff:g>」可執行自動修正"</string>
+    <!-- no translation found for spoken_auto_correct (8005997889020109763) -->
+    <skip />
+    <!-- no translation found for spoken_auto_correct_obscured (6276420476908833791) -->
+    <skip />
     <string name="spoken_description_unknown" msgid="3197434010402179157">"按鍵代碼 %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift 鍵"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"Shift 鍵已開啟 (輕按即可停用)"</string>
diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml
index 2dafde9..d245c32 100644
--- a/java/res/values-zu/strings.xml
+++ b/java/res/values-zu/strings.xml
@@ -84,8 +84,8 @@
     <string name="spoken_use_headphones" msgid="896961781287283493">"Plaka ku-headset ukuze uzwe okhiye bephasiwedi ezindlebeni zakho bezwakala kakhulu."</string>
     <string name="spoken_current_text_is" msgid="2485723011272583845">"Umbhalo wamanje ngu %s"</string>
     <string name="spoken_no_text_entered" msgid="7479685225597344496">"Awukho umbhalo ofakiwe"</string>
-    <string name="spoken_auto_correct" msgid="5381764628886369268">"I-<xliff:g id="KEY">%1$s</xliff:g> ilungisa i-<xliff:g id="ORIGINAL">%2$s</xliff:g> ibe yi-<xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
-    <string name="spoken_auto_correct_obscured" msgid="1186884531440481089">"I-<xliff:g id="KEY">%1$s</xliff:g> inokulungiswa okuzenzakalelayo"</string>
+    <string name="spoken_auto_correct" msgid="8005997889020109763">"I-<xliff:g id="KEY">%1$s</xliff:g> ilungisa i-<xliff:g id="ORIGINAL_WORD">%2$s</xliff:g> ibe yi-<xliff:g id="CORRECTED">%3$s</xliff:g>"</string>
+    <string name="spoken_auto_correct_obscured" msgid="6276420476908833791">"I-<xliff:g id="KEY">%1$s</xliff:g> yenza ukulungiswa kokuzenzakalela"</string>
     <string name="spoken_description_unknown" msgid="3197434010402179157">"Ikhodi yokhiye %d"</string>
     <string name="spoken_description_shift" msgid="244197883292549308">"Shift"</string>
     <string name="spoken_description_shift_shifted" msgid="1681877323344195035">"U-Shift uvuliwe (thepha ukuwuvimbela)"</string>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 0978214..31945d0 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -26,8 +26,8 @@
         <attr name="keyboardViewStyle" format="reference" />
         <!-- MainKeyboardView style -->
         <attr name="mainKeyboardViewStyle" format="reference" />
-        <!-- EmojiKeyboardView style -->
-        <attr name="emojiKeyboardViewStyle" format="reference" />
+        <!-- EmojiPalettesView style -->
+        <attr name="emojiPalettesViewStyle" format="reference" />
         <!-- MoreKeysKeyboard style -->
         <attr name="moreKeysKeyboardStyle" format="reference" />
         <!-- MoreKeysKeyboardView style -->
@@ -167,7 +167,7 @@
         <attr name="suppressKeyPreviewAfterBatchInputDuration" format="integer" />
     </declare-styleable>
 
-    <declare-styleable name="EmojiKeyboardView">
+    <declare-styleable name="EmojiPalettesView">
         <attr name="emojiTabLabelColor" format="reference" />
     </declare-styleable>
 
diff --git a/java/res/values/colors.xml b/java/res/values/colors.xml
index 3803cb7..94fadb9 100644
--- a/java/res/values/colors.xml
+++ b/java/res/values/colors.xml
@@ -39,7 +39,6 @@
     <color name="typed_word_color_ics">#D833B5E5</color>
     <color name="suggested_word_color_ics">#B233B5E5</color>
     <color name="highlight_translucent_color_ics">#9933B5E5</color>
-    <color name="key_text_color_ics">@android:color/white</color>
     <color name="key_text_shadow_color_ics">@android:color/transparent</color>
     <color name="key_text_inactivated_color_ics">#66E0E4E5</color>
     <color name="key_hint_letter_color_ics">#80000000</color>
@@ -66,4 +65,7 @@
     <!-- TODO: Color which should be included in the theme -->
     <color name="emoji_key_background_color">#00000000</color>
     <color name="emoji_key_pressed_background_color">#30FFFFFF</color>
+
+    <color name="key_text_color_normal_ics">@android:color/white</color>
+    <color name="key_text_color_functional_ics">@android:color/white</color>
 </resources>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 465d52c..66b9b70 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -103,7 +103,7 @@
      -->
     <string-array name="auto_correction_threshold_values" translatable="false">
         <!-- Off, When auto correction setting is Off, this value is not used. -->
-        <item></item>
+        <item>floatMaxValue</item>
         <!-- Modest : Suggestion whose normalized score is greater than this value
              will be subject to auto-correction. -->
         <item>0.185</item>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 2d626db..18cb262 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -118,7 +118,7 @@
     <!-- Emoji keyboard -->
     <fraction name="emoji_keyboard_key_width">14.2857%p</fraction>
     <fraction name="emoji_keyboard_row_height">33%p</fraction>
-    <fraction name="emoji_keyboard_key_letter_size">81%p</fraction>
+    <fraction name="emoji_keyboard_key_letter_size">68%p</fraction>
     <integer name="emoji_keyboard_max_key_count">21</integer>
     <dimen name="emoji_category_page_id_height">3dp</dimen>
 
diff --git a/java/res/values/donottranslate.xml b/java/res/values/donottranslate.xml
index 42e692d..4733aa2 100644
--- a/java/res/values/donottranslate.xml
+++ b/java/res/values/donottranslate.xml
@@ -31,6 +31,9 @@
     <string name="symbols_word_separators">"&#x0009;&#x0020;\n"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
     <!-- Word connectors -->
     <string name="symbols_word_connectors">\'-</string>
+    <!-- The sentence separator code point, for capitalization -->
+    <!-- U+002E: "." FULL STOP   ; 2Eh = 46d -->
+    <integer name="sentence_separator">46</integer>
     <!-- Whether this language uses spaces between words -->
     <bool name="current_language_has_spaces">true</bool>
 
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 06f4b47..a779c6e 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -174,10 +174,10 @@
     <!-- Spoken description when there is no text entered -->
     <string name="spoken_no_text_entered">No text entered</string>
 
-    <!-- Spoken description to let the user know what auto-correction will be performed when a key is pressed. -->
-    <string name="spoken_auto_correct"><xliff:g id="key" example="Space">%1$s</xliff:g> corrects <xliff:g id="original">%2$s</xliff:g> to <xliff:g id="corrected">%3$s</xliff:g></string>
+    <!-- Spoken description to let the user know what auto-correction will be performed when a key is pressed. An auto-correction replaces a single word with one or more words. -->
+    <string name="spoken_auto_correct"><xliff:g id="key" example="Space">%1$s</xliff:g> corrects <xliff:g id="original_word">%2$s</xliff:g> to <xliff:g id="corrected">%3$s</xliff:g></string>
     <!-- Spoken description used during obscured (e.g. password) entry to let the user know that auto-correction will be performed when a key is pressed. -->
-    <string name="spoken_auto_correct_obscured"><xliff:g id="key" example="Space">%1$s</xliff:g> has auto-correction</string>
+    <string name="spoken_auto_correct_obscured"><xliff:g id="key" example="Space">%1$s</xliff:g> performs auto-correction</string>
 
     <!-- Spoken description for unknown keyboard keys. -->
     <string name="spoken_description_unknown">Key code %d</string>
diff --git a/java/res/values/themes-common.xml b/java/res/values/themes-common.xml
index 8e9cfc9..3760771 100644
--- a/java/res/values/themes-common.xml
+++ b/java/res/values/themes-common.xml
@@ -104,10 +104,10 @@
     <style
         name="MainKeyboardView"
         parent="KeyboardView" />
-    <!-- Though {@link EmojiKeyboardView} doesn't extend {@link KeyboardView}, some views inside it,
+    <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
-        name="EmojiKeyboardView"
+        name="EmojiPalettesView"
         parent="KeyboardView"
     >
         <item name="emojiTabLabelColor">@color/emoji_tab_label_color_ics</item>
diff --git a/java/res/values/themes-gb.xml b/java/res/values/themes-gb.xml
index d9ac4ac..f52695f 100644
--- a/java/res/values/themes-gb.xml
+++ b/java/res/values/themes-gb.xml
@@ -23,7 +23,7 @@
         <item name="keyboardStyle">@style/Keyboard.GB</item>
         <item name="keyboardViewStyle">@style/KeyboardView.GB</item>
         <item name="mainKeyboardViewStyle">@style/MainKeyboardView.GB</item>
-        <item name="emojiKeyboardViewStyle">@style/EmojiKeyboardView.GB</item>
+        <item name="emojiPalettesViewStyle">@style/EmojiPalettesView.GB</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.GB</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.GB</item>
         <item name="moreKeysKeyboardContainerStyle">@style/MoreKeysKeyboardContainer.GB</item>
@@ -96,10 +96,10 @@
         <item name="spacebarTextColor">@color/spacebar_text_color_gb</item>
         <item name="spacebarTextShadowColor">@color/spacebar_text_shadow_color_gb</item>
     </style>
-    <!-- Though {@link EmojiKeyboardView} doesn't extend {@link KeyboardView}, some views inside it,
+    <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
-        name="EmojiKeyboardView.GB"
+        name="EmojiPalettesView.GB"
         parent="KeyboardView.GB"
     >
         <item name="keyBackground">@drawable/btn_keyboard_key_functional_gb</item>
diff --git a/java/res/values/themes-ics.xml b/java/res/values/themes-ics.xml
index 33dd50c..a77e685 100644
--- a/java/res/values/themes-ics.xml
+++ b/java/res/values/themes-ics.xml
@@ -23,7 +23,7 @@
         <item name="keyboardStyle">@style/Keyboard.ICS</item>
         <item name="keyboardViewStyle">@style/KeyboardView.ICS</item>
         <item name="mainKeyboardViewStyle">@style/MainKeyboardView.ICS</item>
-        <item name="emojiKeyboardViewStyle">@style/EmojiKeyboardView.ICS</item>
+        <item name="emojiPalettesViewStyle">@style/EmojiPalettesView.ICS</item>
         <item name="moreKeysKeyboardStyle">@style/MoreKeysKeyboard.ICS</item>
         <item name="moreKeysKeyboardViewStyle">@style/MoreKeysKeyboardView.ICS</item>
         <item name="moreKeysKeyboardContainerStyle">@style/MoreKeysKeyboardContainer.ICS</item>
@@ -97,10 +97,10 @@
         <item name="spacebarTextColor">@color/spacebar_text_color_ics</item>
         <item name="spacebarTextShadowColor">@color/spacebar_text_shadow_color_ics</item>
     </style>
-    <!-- Though {@link EmojiKeyboardView} doesn't extend {@link KeyboardView}, some views inside it,
+    <!-- Though {@link EmojiPalettesView} doesn't extend {@link KeyboardView}, some views inside it,
          for instance delete button, need themed {@link KeyboardView} attributes. -->
     <style
-        name="EmojiKeyboardView.ICS"
+        name="EmojiPalettesView.ICS"
         parent="KeyboardView.ICS"
     >
         <item name="keyBackgroundEmojiFunctional">@drawable/btn_keyboard_key_functional_ics</item>
diff --git a/java/res/xml-sw600dp/keys_comma_period.xml b/java/res/xml-sw600dp/keys_comma_period.xml
new file mode 100644
index 0000000..7604e03
--- /dev/null
+++ b/java/res/xml-sw600dp/keys_comma_period.xml
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 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.
+*/
+-->
+
+<merge
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <switch>
+        <case
+            latin:mode="email|url"
+        >
+            <Key
+                latin:keyLabel=","
+                latin:keyHintLabel="-"
+                latin:moreKeys="-"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+            <Key
+                latin:keyLabel="."
+                latin:keyHintLabel="_"
+                latin:moreKeys="_"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+        </case>
+        <case
+            latin:languageCode="ar"
+        >
+            <Key
+                latin:keyLabel="!text/keylabel_for_apostrophe"
+                latin:keyHintLabel="!text/keyhintlabel_for_apostrophe"
+                latin:moreKeys="!text/more_keys_for_apostrophe"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+            <Key
+                latin:keyLabel="."
+                latin:keyHintLabel="!text/keyhintlabel_for_arabic_diacritics"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/more_keys_for_arabic_diacritics"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+        </case>
+        <case
+            latin:languageCode="fa"
+        >
+            <Key
+                latin:keyLabel="!text/keylabel_for_apostrophe"
+                latin:keyHintLabel="!text/keyhintlabel_for_apostrophe"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/more_keys_for_apostrophe"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+            <Key
+                latin:keyLabel="."
+                latin:keyHintLabel="!text/keyhintlabel_for_arabic_diacritics"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/more_keys_for_arabic_diacritics"
+                latin:backgroundType="functional"
+                latin:keyStyle="hasShiftedLetterHintStyle" />
+        </case>
+        <case
+            latin:languageCode="hy"
+        >
+            <!-- U+055D: "՝" ARMENIAN COMMA -->
+            <Key
+                latin:keyLabel="&#x055D;"
+                latin:backgroundType="functional" />
+            <!-- U+0589: "։" ARMENIAN FULL STOP -->
+            <Key
+                latin:keyLabel="&#x0589;"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:backgroundType="functional"
+                latin:moreKeys="!text/more_keys_for_punctuation" />
+        </case>
+        <default>
+            <Key
+                latin:keyLabel="!text/keylabel_for_tablet_comma"
+                latin:keyHintLabel="!text/keyhintlabel_for_tablet_comma"
+                latin:backgroundType="functional"
+                latin:moreKeys="!text/more_keys_for_tablet_comma" />
+            <Key
+                latin:keyLabel="."
+                latin:keyHintLabel="!text/keyhintlabel_for_period"
+                latin:backgroundType="functional"
+                latin:moreKeys="!text/more_keys_for_period" />
+        </default>
+    </switch>
+</merge>
diff --git a/java/res/xml-sw600dp/keys_exclamation_question.xml b/java/res/xml-sw600dp/keys_exclamation_question.xml
index 983ef38..cd38282 100644
--- a/java/res/xml-sw600dp/keys_exclamation_question.xml
+++ b/java/res/xml-sw600dp/keys_exclamation_question.xml
@@ -22,7 +22,7 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <Key
-        latin:keyLabel="\?" />
-    <Key
         latin:keyLabel="!" />
+    <Key
+        latin:keyLabel="\?" />
 </merge>
diff --git a/java/res/xml/key_styles_enter.xml b/java/res/xml/key_styles_enter.xml
index 5976e95..568c602 100644
--- a/java/res/xml/key_styles_enter.xml
+++ b/java/res/xml/key_styles_enter.xml
@@ -21,11 +21,14 @@
 <merge
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
+    <!-- TODO: Stop using many conditional cases for emoji_key_as_more_key. There are way too many to maintain. -->
     <!-- Navigate more keys style -->
     <switch>
+        <!-- latin:passwordInput="true" -->
         <case
             latin:imeAction="actionNext"
             latin:navigatePrevious="true"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle"
@@ -35,6 +38,7 @@
         <case
             latin:imeAction="actionNext"
             latin:navigatePrevious="false"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle" />
@@ -42,6 +46,7 @@
         <case
             latin:imeAction="actionPrevious"
             latin:navigateNext="true"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle"
@@ -51,14 +56,15 @@
         <case
             latin:imeAction="actionPrevious"
             latin:navigateNext="false"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle" />
         </case>
-        <!-- imeAction!="actionNext" and imeAction!="actionPrevious" -->
         <case
             latin:navigateNext="true"
             latin:navigatePrevious="true"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle"
@@ -68,6 +74,7 @@
         <case
             latin:navigateNext="true"
             latin:navigatePrevious="false"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle"
@@ -77,13 +84,166 @@
         <case
             latin:navigateNext="false"
             latin:navigatePrevious="true"
+            latin:passwordInput="true"
         >
             <key-style
                 latin:styleName="navigateMoreKeysStyle"
                 latin:keyLabelFlags="hasPopupHint|preserveCase"
                 latin:moreKeys="!text/action_previous_as_more_key" />
         </case>
-        <!-- naviagteNext="false" and navigatePrevious="false" -->
+        <case
+            latin:navigateNext="false"
+            latin:navigatePrevious="false"
+            latin:passwordInput="true"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle" />
+        </case>
+        <!-- latin:mode="email|url|phone|number|date|time|datetime" -->
+        <case
+            latin:imeAction="actionNext"
+            latin:navigatePrevious="true"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!text/action_previous_as_more_key" />
+        </case>
+        <case
+            latin:imeAction="actionNext"
+            latin:navigatePrevious="false"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle" />
+        </case>
+        <case
+            latin:imeAction="actionPrevious"
+            latin:navigateNext="true"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:imeAction="actionPrevious"
+            latin:navigateNext="false"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle" />
+        </case>
+        <case
+            latin:navigateNext="true"
+            latin:navigatePrevious="true"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!2,!needsDividers!,!text/action_previous_as_more_key,!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="true"
+            latin:navigatePrevious="false"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="false"
+            latin:navigatePrevious="true"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!text/action_previous_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="false"
+            latin:navigatePrevious="false"
+            latin:mode="email|url|phone|number|date|time|datetime"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle" />
+        </case>
+        <!-- default -->
+        <case
+            latin:imeAction="actionNext"
+            latin:navigatePrevious="true"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!2,!needsDividers!,!text/emoji_key_as_more_key,!text/action_previous_as_more_key" />
+        </case>
+        <case
+            latin:imeAction="actionNext"
+            latin:navigatePrevious="false"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:moreKeys="!text/emoji_key_as_more_key" />
+        </case>
+        <case
+            latin:imeAction="actionPrevious"
+            latin:navigateNext="true"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!2,!needsDividers!,!text/emoji_key_as_more_key,!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:imeAction="actionPrevious"
+            latin:navigateNext="false"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:moreKeys="!text/emoji_key_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="true"
+            latin:navigatePrevious="true"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!3,!needsDividers!,!text/emoji_key_as_more_key,!text/action_previous_as_more_key,!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="true"
+            latin:navigatePrevious="false"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!2,!needsDividers!,!text/emoji_key_as_more_key,!text/action_next_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="false"
+            latin:navigatePrevious="true"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:keyLabelFlags="hasPopupHint|preserveCase"
+                latin:moreKeys="!fixedColumnOrder!2,!needsDividers!,!text/emoji_key_as_more_key,!text/action_previous_as_more_key" />
+        </case>
+        <case
+            latin:navigateNext="false"
+            latin:navigatePrevious="false"
+        >
+            <key-style
+                latin:styleName="navigateMoreKeysStyle"
+                latin:moreKeys="!text/emoji_key_as_more_key" />
+        </case>
         <default>
             <key-style
                 latin:styleName="navigateMoreKeysStyle" />
diff --git a/java/res/xml/keys_comma_period.xml b/java/res/xml/keys_comma_period.xml
index 02b46c2..1b51e45 100644
--- a/java/res/xml/keys_comma_period.xml
+++ b/java/res/xml/keys_comma_period.xml
@@ -23,22 +23,6 @@
 >
     <switch>
         <case
-            latin:mode="email|url"
-        >
-            <Key
-                latin:keyLabel="."
-                latin:keyHintLabel="_"
-                latin:moreKeys="_"
-                latin:backgroundType="functional"
-                latin:keyStyle="hasShiftedLetterHintStyle" />
-            <Key
-                latin:keyLabel=","
-                latin:keyHintLabel="-"
-                latin:moreKeys="-"
-                latin:backgroundType="functional"
-                latin:keyStyle="hasShiftedLetterHintStyle" />
-        </case>
-        <case
             latin:languageCode="ar"
         >
             <Key
@@ -76,28 +60,28 @@
         <case
             latin:languageCode="hy"
         >
+            <!-- U+055D: "՝" ARMENIAN COMMA -->
+            <Key
+                latin:keyLabel="&#x055D;"
+                latin:backgroundType="functional" />
             <!-- U+0589: "։" ARMENIAN FULL STOP -->
             <Key
                 latin:keyLabel="&#x0589;"
                 latin:keyLabelFlags="hasPopupHint"
                 latin:backgroundType="functional"
                 latin:moreKeys="!text/more_keys_for_punctuation" />
-            <!-- U+055D: "՝" ARMENIAN COMMA -->
-            <Key
-                latin:keyLabel="&#x055D;"
-                latin:backgroundType="functional" />
         </case>
         <default>
             <Key
-                latin:keyLabel="."
-                latin:keyHintLabel="!text/keyhintlabel_for_tablet_period"
-                latin:backgroundType="functional"
-                latin:moreKeys="!text/more_keys_for_tablet_period" />
-            <Key
                 latin:keyLabel="!text/keylabel_for_tablet_comma"
                 latin:keyHintLabel="!text/keyhintlabel_for_tablet_comma"
                 latin:backgroundType="functional"
                 latin:moreKeys="!text/more_keys_for_tablet_comma" />
+            <Key
+                latin:keyLabel="."
+                latin:keyHintLabel="!text/keyhintlabel_for_period"
+                latin:backgroundType="functional"
+                latin:moreKeys="!text/more_keys_for_period" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/rowkeys_symbols3.xml b/java/res/xml/rowkeys_symbols3.xml
index 9f5e620..074078c 100644
--- a/java/res/xml/rowkeys_symbols3.xml
+++ b/java/res/xml/rowkeys_symbols3.xml
@@ -54,9 +54,9 @@
         latin:keyLabel="!text/keylabel_for_symbols_semicolon"
         latin:moreKeys="!text/more_keys_for_symbols_semicolon" />
     <Key
-        latin:keyLabel="!text/keylabel_for_symbols_question"
-        latin:moreKeys="!text/more_keys_for_symbols_question" />
-    <Key
         latin:keyLabel="!"
         latin:moreKeys="!text/more_keys_for_symbols_exclamation" />
+    <Key
+        latin:keyLabel="!text/keylabel_for_symbols_question"
+        latin:moreKeys="!text/more_keys_for_symbols_question" />
 </merge>
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
new file mode 100644
index 0000000..b119d6c
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
@@ -0,0 +1,56 @@
+/*
+ * 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.compat;
+
+import android.os.Build;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.lang.reflect.Constructor;
+
+public final class InputMethodSubtypeCompatUtils {
+    private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName();
+    // Note that InputMethodSubtype(int nameId, int iconId, String locale, String mode,
+    // String extraValue, boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, int id)
+    // has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
+    private static final Constructor<?> CONSTRUCTOR_INPUT_METHOD_SUBTYPE =
+            CompatUtils.getConstructor(InputMethodSubtype.class,
+                    Integer.TYPE, Integer.TYPE, String.class, String.class, String.class,
+                    Boolean.TYPE, Boolean.TYPE, Integer.TYPE);
+    static {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null) {
+                android.util.Log.w(TAG, "Warning!!! Constructor is not defined.");
+            }
+        }
+    }
+    private InputMethodSubtypeCompatUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static InputMethodSubtype newInputMethodSubtype(int nameId, int iconId, String locale,
+            String mode, String extraValue, boolean isAuxiliary,
+            boolean overridesImplicitlyEnabledSubtype, int id) {
+        if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null
+                || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            return new InputMethodSubtype(nameId, iconId, locale, mode, extraValue, isAuxiliary,
+                    overridesImplicitlyEnabledSubtype);
+        }
+        return (InputMethodSubtype) CompatUtils.newInstance(CONSTRUCTOR_INPUT_METHOD_SUBTYPE,
+                nameId, iconId, locale, mode, extraValue, isAuxiliary,
+                overridesImplicitlyEnabledSubtype, id);
+    }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java
index 6841652..4366348 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.dictionarypack;
 
+import com.android.inputmethod.latin.utils.FragmentUtils;
+
 import android.content.Intent;
 import android.os.Bundle;
 import android.preference.PreferenceActivity;
@@ -24,6 +26,8 @@
  * Preference screen.
  */
 public final class DictionarySettingsActivity extends PreferenceActivity {
+    private static final String DEFAULT_FRAGMENT = DictionarySettingsFragment.class.getName();
+
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -32,11 +36,17 @@
     @Override
     public Intent getIntent() {
         final Intent modIntent = new Intent(super.getIntent());
-        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DictionarySettingsFragment.class.getName());
+        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
         modIntent.putExtra(EXTRA_NO_HEADERS, true);
         // Important note : the original intent should contain a String extra with the key
         // DictionarySettingsFragment.DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT so that the
         // fragment can know who the client is.
         return modIntent;
     }
+
+    // TODO: Uncomment the override annotation once we start using SDK version 19.
+    // @Override
+    public boolean isValidFragment(String fragmentName) {
+        return FragmentUtils.isValidFragment(fragmentName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
index fed134e..e23131a 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
@@ -50,8 +50,9 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        if (mCategoryPageSize == 0) {
-            // If the category is not set yet, just clear and return.
+        if (mCategoryPageSize <= 1) {
+            // If the category is not set yet or contains only one category,
+            // just clear and return.
             canvas.drawColor(0);
             return;
         }
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
index 267fad5..ceb44e7 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
@@ -56,7 +56,7 @@
                 - (mKeyVerticalGap - mBottomPadding) / 2;
         mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight
                 - mEmojiCategoryPageIdViewHeight;
-        mEmojiPagerBottomMargin = mKeyVerticalGap / 2;
+        mEmojiPagerBottomMargin = 0;
         mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1;
     }
 
@@ -75,9 +75,7 @@
 
     public void setActionBarProperties(LinearLayout ll) {
         final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams();
-        lp.height = mEmojiActionBarHeight;
-        lp.topMargin = 0;
-        lp.bottomMargin = mBottomPadding;
+        lp.height = mEmojiActionBarHeight - mBottomPadding;
         ll.setLayoutParams(lp);
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
similarity index 88%
rename from java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
rename to java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
index eb48d01..9779c68 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
@@ -60,8 +60,8 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * View class to implement Emoji keyboards.
- * The Emoji keyboard consists of group of views {@link R.layout#emoji_keyboard_view}.
+ * View class to implement Emoji palettes.
+ * The Emoji keyboard consists of group of views {@link R.layout#emoji_palettes_view}.
  * <ol>
  * <li> Emoji category tabs.
  * <li> Delete button.
@@ -70,19 +70,21 @@
  * </ol>
  * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
  */
-public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener,
+public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener,
         ViewPager.OnPageChangeListener, View.OnClickListener,
         ScrollKeyboardView.OnKeyClickListener {
-    private static final String TAG = EmojiKeyboardView.class.getSimpleName();
+    private static final String TAG = EmojiPalettesView.class.getSimpleName();
+    private static final boolean DEBUG_PAGER = false;
     private final int mKeyBackgroundId;
     private final int mEmojiFunctionalKeyBackgroundId;
     private final KeyboardLayoutSet mLayoutSet;
     private final ColorStateList mTabLabelColor;
     private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
-    private EmojiKeyboardAdapter mEmojiKeyboardAdapter;
+    private EmojiPalettesAdapter mEmojiPalettesAdapter;
 
     private TabHost mTabHost;
     private ViewPager mEmojiPager;
+    private int mCurrentPagerPosition = 0;
     private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
 
     private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
@@ -378,11 +380,11 @@
 
     private final EmojiCategory mEmojiCategory;
 
-    public EmojiKeyboardView(final Context context, final AttributeSet attrs) {
-        this(context, attrs, R.attr.emojiKeyboardViewStyle);
+    public EmojiPalettesView(final Context context, final AttributeSet attrs) {
+        this(context, attrs, R.attr.emojiPalettesViewStyle);
     }
 
-    public EmojiKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
+    public EmojiPalettesView(final Context context, final AttributeSet attrs, final int defStyle) {
         super(context, attrs, defStyle);
         final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
                 R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
@@ -391,11 +393,11 @@
         mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
                 R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0);
         keyboardViewAttr.recycle();
-        final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs,
-                R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView);
-        mTabLabelColor = emojiKeyboardViewAttr.getColorStateList(
-                R.styleable.EmojiKeyboardView_emojiTabLabelColor);
-        emojiKeyboardViewAttr.recycle();
+        final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs,
+                R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView);
+        mTabLabelColor = emojiPalettesViewAttr.getColorStateList(
+                R.styleable.EmojiPalettesView_emojiTabLabelColor);
+        emojiPalettesViewAttr.recycle();
         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
                 context, null /* editorInfo */);
         final Resources res = context.getResources();
@@ -453,12 +455,13 @@
         mTabHost.setOnTabChangedListener(this);
         mTabHost.getTabWidget().setStripEnabled(true);
 
-        mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(mEmojiCategory, mLayoutSet, this);
+        mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mLayoutSet, this);
 
         mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
-        mEmojiPager.setAdapter(mEmojiKeyboardAdapter);
+        mEmojiPager.setAdapter(mEmojiPalettesAdapter);
         mEmojiPager.setOnPageChangeListener(this);
         mEmojiPager.setOffscreenPageLimit(0);
+        mEmojiPager.setPersistentDrawingCache(ViewPager.PERSISTENT_NO_CACHE);
         final Resources res = getResources();
         final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
         emojiLp.setPagerProperties(mEmojiPager);
@@ -484,10 +487,10 @@
         spaceKey.setTag(Constants.CODE_SPACE);
         spaceKey.setOnClickListener(this);
         emojiLp.setKeyProperties(spaceKey);
-        final ImageView sendKey = (ImageView)findViewById(R.id.emoji_keyboard_send);
-        sendKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
-        sendKey.setTag(Constants.CODE_ENTER);
-        sendKey.setOnClickListener(this);
+        final ImageView alphabetKey2 = (ImageView)findViewById(R.id.emoji_keyboard_alphabet2);
+        alphabetKey2.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
+        alphabetKey2.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL);
+        alphabetKey2.setOnClickListener(this);
     }
 
     @Override
@@ -505,6 +508,7 @@
         setCurrentCategoryId(newPos.first /* categoryId */, false /* force */);
         mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */);
         updateEmojiCategoryPageIdView();
+        mCurrentPagerPosition = position;
     }
 
     @Override
@@ -551,7 +555,7 @@
 
     @Override
     public void onKeyClick(final Key key) {
-        mEmojiKeyboardAdapter.addRecentKey(key);
+        mEmojiPalettesAdapter.addRecentKey(key);
         mEmojiCategory.saveLastTypedCategoryPage();
         final int code = key.getCode();
         if (code == Constants.CODE_OUTPUT_TEXT) {
@@ -565,6 +569,22 @@
         // TODO:
     }
 
+    public void startEmojiPalettes() {
+        if (DEBUG_PAGER) {
+            Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition);
+        }
+        mEmojiPager.setAdapter(mEmojiPalettesAdapter);
+        mEmojiPager.setCurrentItem(mCurrentPagerPosition);
+    }
+
+    public void stopEmojiPalettes() {
+        if (DEBUG_PAGER) {
+            Log.d(TAG, "deallocate emoji palettes memory");
+        }
+        mEmojiPalettesAdapter.flushPendingRecentKeys();
+        mEmojiPager.setAdapter(null);
+    }
+
     public void setKeyboardActionListener(final KeyboardActionListener listener) {
         mKeyboardActionListener = listener;
         mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
@@ -580,10 +600,18 @@
     }
 
     private void setCurrentCategoryId(final int categoryId, final boolean force) {
-        if (mEmojiCategory.getCurrentCategoryId() == categoryId && !force) {
+        final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
+        if (oldCategoryId == categoryId && !force) {
             return;
         }
 
+        if (oldCategoryId == CATEGORY_ID_RECENTS) {
+            // Needs to save pending updates for recent keys when we get out of the recents
+            // category because we don't want to move the recent emojis around while the user
+            // is in the recents category.
+            mEmojiPalettesAdapter.flushPendingRecentKeys();
+        }
+
         mEmojiCategory.setCurrentCategoryId(categoryId);
         final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
         final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
@@ -596,15 +624,15 @@
         }
     }
 
-    private static class EmojiKeyboardAdapter extends PagerAdapter {
+    private static class EmojiPalettesAdapter extends PagerAdapter {
         private final ScrollKeyboardView.OnKeyClickListener mListener;
         private final DynamicGridKeyboard mRecentsKeyboard;
-        private final SparseArray<ScrollKeyboardView> mActiveKeyboardView =
+        private final SparseArray<ScrollKeyboardView> mActiveKeyboardViews =
                 CollectionUtils.newSparseArray();
         private final EmojiCategory mEmojiCategory;
         private int mActivePosition = 0;
 
-        public EmojiKeyboardAdapter(final EmojiCategory emojiCategory,
+        public EmojiPalettesAdapter(final EmojiCategory emojiCategory,
                 final KeyboardLayoutSet layoutSet,
                 final ScrollKeyboardView.OnKeyClickListener listener) {
             mEmojiCategory = emojiCategory;
@@ -612,13 +640,23 @@
             mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0);
         }
 
+        public void flushPendingRecentKeys() {
+            mRecentsKeyboard.flushPendingRecentKeys();
+            final KeyboardView recentKeyboardView =
+                    mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
+            if (recentKeyboardView != null) {
+                recentKeyboardView.invalidateAllKeys();
+            }
+        }
+
         public void addRecentKey(final Key key) {
             if (mEmojiCategory.isInRecentTab()) {
+                mRecentsKeyboard.addPendingKey(key);
                 return;
             }
             mRecentsKeyboard.addKeyFirst(key);
             final KeyboardView recentKeyboardView =
-                    mActiveKeyboardView.get(mEmojiCategory.getRecentTabId());
+                    mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId());
             if (recentKeyboardView != null) {
                 recentKeyboardView.invalidateAllKeys();
             }
@@ -634,7 +672,7 @@
             if (mActivePosition == position) {
                 return;
             }
-            final ScrollKeyboardView oldKeyboardView = mActiveKeyboardView.get(mActivePosition);
+            final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition);
             if (oldKeyboardView != null) {
                 oldKeyboardView.releaseCurrentKey();
                 oldKeyboardView.deallocateMemory();
@@ -644,6 +682,15 @@
 
         @Override
         public Object instantiateItem(final ViewGroup container, final int position) {
+            if (DEBUG_PAGER) {
+                Log.d(TAG, "instantiate item: " + position);
+            }
+            final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position);
+            if (oldKeyboardView != null) {
+                oldKeyboardView.deallocateMemory();
+                // This may be redundant but wanted to be safer..
+                mActiveKeyboardViews.remove(position);
+            }
             final Keyboard keyboard =
                     mEmojiCategory.getKeyboardFromPagePosition(position);
             final LayoutInflater inflater = LayoutInflater.from(container.getContext());
@@ -657,7 +704,7 @@
                     R.id.emoji_keyboard_scroller);
             keyboardView.setScrollView(scrollView);
             container.addView(view);
-            mActiveKeyboardView.put(position, keyboardView);
+            mActiveKeyboardViews.put(position, keyboardView);
             return view;
         }
 
@@ -669,12 +716,19 @@
         @Override
         public void destroyItem(final ViewGroup container, final int position,
                 final Object object) {
-            final ScrollKeyboardView keyboardView = mActiveKeyboardView.get(position);
+            if (DEBUG_PAGER) {
+                Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName());
+            }
+            final ScrollKeyboardView keyboardView = mActiveKeyboardViews.get(position);
             if (keyboardView != null) {
                 keyboardView.deallocateMemory();
-                mActiveKeyboardView.remove(position);
+                mActiveKeyboardViews.remove(position);
             }
-            container.removeView(keyboardView);
+            if (object instanceof View) {
+                container.removeView((View)object);
+            } else {
+                Log.w(TAG, "Warning!!! Emoji palette may be leaking. " + object);
+            }
         }
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 3ea6880..f7ec950 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -139,6 +139,8 @@
 
     private final OptionalAttributes mOptionalAttributes;
 
+    private static final int DEFAULT_TEXT_COLOR = 0xFFFFFFFF;
+
     private static final class OptionalAttributes {
         /** Text to output when pressed. This can be multiple characters, like ".com" */
         public final String mOutputText;
@@ -602,7 +604,22 @@
     }
 
     public final int selectTextColor(final KeyDrawParams params) {
-        return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
+        if (isShiftedLetterActivated()) {
+            return params.mTextInactivatedColor;
+        }
+        if (params.mTextColorStateList == null) {
+            return DEFAULT_TEXT_COLOR;
+        }
+        final int[] state;
+        // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels.
+        // Currently, we distinguish "input key" from "functional key" by checking the
+        // length of the label( > 1) and "functional" attributes (= true).
+        if (mLabel != null && mLabel.length() > 1) {
+            state = getCurrentDrawableState();
+        } else {
+            state = KEY_STATE_NORMAL;
+        }
+        return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR);
     }
 
     public final int selectHintTextSize(final KeyDrawParams params) {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 74edd87..9760983 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -68,7 +68,7 @@
     private InputView mCurrentInputView;
     private View mMainKeyboardFrame;
     private MainKeyboardView mKeyboardView;
-    private EmojiKeyboardView mEmojiKeyboardView;
+    private EmojiPalettesView mEmojiPalettesView;
     private LatinIME mLatinIME;
     private Resources mResources;
 
@@ -155,7 +155,7 @@
     }
 
     public void saveKeyboardState() {
-        if (getKeyboard() != null) {
+        if (getKeyboard() != null || isShowingEmojiKeyboard()) {
             mState.onSaveKeyboardState();
         }
     }
@@ -169,7 +169,7 @@
     }
 
     private void setKeyboard(final Keyboard keyboard) {
-        // Make {@link MainKeyboardView} visible and hide {@link EmojiKeyboardView}.
+        // Make {@link MainKeyboardView} visible and hide {@link EmojiPalettesView}.
         setMainKeyboardFrame();
         final MainKeyboardView keyboardView = mKeyboardView;
         final Keyboard oldKeyboard = keyboardView.getKeyboard();
@@ -259,14 +259,16 @@
 
     private void setMainKeyboardFrame() {
         mMainKeyboardFrame.setVisibility(View.VISIBLE);
-        mEmojiKeyboardView.setVisibility(View.GONE);
+        mEmojiPalettesView.setVisibility(View.GONE);
+        mEmojiPalettesView.stopEmojiPalettes();
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setEmojiKeyboard() {
         mMainKeyboardFrame.setVisibility(View.GONE);
-        mEmojiKeyboardView.setVisibility(View.VISIBLE);
+        mEmojiPalettesView.startEmojiPalettes();
+        mEmojiPalettesView.setVisibility(View.VISIBLE);
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
@@ -315,7 +317,7 @@
     }
 
     public boolean isShowingEmojiKeyboard() {
-        return mEmojiKeyboardView != null && mEmojiKeyboardView.getVisibility() == View.VISIBLE;
+        return mEmojiPalettesView != null && mEmojiPalettesView.getVisibility() == View.VISIBLE;
     }
 
     public boolean isShowingMoreKeysPanel() {
@@ -327,7 +329,7 @@
 
     public View getVisibleKeyboardView() {
         if (isShowingEmojiKeyboard()) {
-            return mEmojiKeyboardView;
+            return mEmojiPalettesView;
         }
         return mKeyboardView;
     }
@@ -336,6 +338,16 @@
         return mKeyboardView;
     }
 
+    public void deallocateMemory() {
+        if (mKeyboardView != null) {
+            mKeyboardView.cancelAllOngoingEvents();
+            mKeyboardView.deallocateMemory();
+        }
+        if (mEmojiPalettesView != null) {
+            mEmojiPalettesView.stopEmojiPalettes();
+        }
+    }
+
     public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) {
         if (mKeyboardView != null) {
             mKeyboardView.closing();
@@ -345,15 +357,15 @@
         mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
                 R.layout.input_view, null);
         mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
-        mEmojiKeyboardView = (EmojiKeyboardView)mCurrentInputView.findViewById(
+        mEmojiPalettesView = (EmojiPalettesView)mCurrentInputView.findViewById(
                 R.id.emoji_keyboard_view);
 
         mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
         mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
         mKeyboardView.setKeyboardActionListener(mLatinIME);
-        mEmojiKeyboardView.setHardwareAcceleratedDrawingEnabled(
+        mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled(
                 isHardwareAcceleratedDrawingEnabled);
-        mEmojiKeyboardView.setKeyboardActionListener(mLatinIME);
+        mEmojiPalettesView.setKeyboardActionListener(mLatinIME);
 
         // This always needs to be set since the accessibility state can
         // potentially change without the input view being re-created.
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index aeb9e67..5578713 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -243,6 +243,8 @@
     }
 
     private void freeOffscreenBuffer() {
+        mOffscreenCanvas.setBitmap(null);
+        mOffscreenCanvas.setMatrix(null);
         if (mOffscreenBuffer != null) {
             mOffscreenBuffer.recycle();
             mOffscreenBuffer = null;
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index ee4ac95..52f190e 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -823,14 +823,16 @@
             final int size = sAggregratedPointers.getPointerSize();
             if (size > sLastRecognitionPointSize
                     && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
-                sLastRecognitionPointSize = size;
-                sLastRecognitionTime = eventTime;
                 if (DEBUG_LISTENER) {
                     Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId,
                             size));
                 }
                 mTimerProxy.startUpdateBatchInputTimer(this);
                 mListener.onUpdateBatchInput(sAggregratedPointers);
+                // The listener may change the size of the pointers (when auto-committing
+                // for example), so we need to get the size from the pointers again.
+                sLastRecognitionPointSize = sAggregratedPointers.getPointerSize();
+                sLastRecognitionTime = eventTime;
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
index c10fdba..4ccecb2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -18,6 +18,8 @@
 
 import com.android.inputmethod.latin.Constants;
 
+import android.text.TextUtils;
+
 /**
  * The string parser of codesArray specification for <GridRows />. The attribute codesArray is an
  * array of string.
@@ -34,7 +36,7 @@
 public final class CodesArrayParser {
     // Constants for parsing.
     private static final char COMMA = ',';
-    private static final char VERTICAL_BAR = '|';
+    private static final String VERTICAL_BAR_STRING = "\\|";
     private static final String COMMA_STRING = ",";
     private static final int BASE_HEX = 16;
 
@@ -43,8 +45,11 @@
     }
 
     private static String getLabelSpec(final String codesArraySpec) {
-        final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
-        return (pos < 0) ? codesArraySpec : codesArraySpec.substring(0, pos);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        if (strs.length <= 1) {
+            return codesArraySpec;
+        }
+        return strs[0];
     }
 
     public static String parseLabel(final String codesArraySpec) {
@@ -58,8 +63,25 @@
     }
 
     private static String getCodeSpec(final String codesArraySpec) {
-        final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
-        return (pos < 0) ? codesArraySpec : codesArraySpec.substring(pos + 1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -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);
+        if (strs.length <= 2) {
+            return 0;
+        }
+        try {
+            return Integer.parseInt(strs[2]);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
     }
 
     public static int parseCode(final String codesArraySpec) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
index 0dd71e2..09766ac 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -20,7 +20,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import com.android.inputmethod.keyboard.EmojiKeyboardView;
+import com.android.inputmethod.keyboard.EmojiPalettesView;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.settings.Settings;
@@ -39,15 +39,16 @@
     private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
     private static final int TEMPLATE_KEY_CODE_0 = 0x30;
     private static final int TEMPLATE_KEY_CODE_1 = 0x31;
+    private final Object mLock = new Object();
 
     private final SharedPreferences mPrefs;
-    private final int mLeftPadding;
     private final int mHorizontalStep;
     private final int mVerticalStep;
     private final int mColumnsNum;
     private final int mMaxKeyCount;
     private final boolean mIsRecents;
     private final ArrayDeque<GridKey> mGridKeys = CollectionUtils.newArrayDeque();
+    private final ArrayDeque<Key> mPendingKeys = CollectionUtils.newArrayDeque();
 
     private Key[] mCachedGridKeys;
 
@@ -56,12 +57,11 @@
         super(templateKeyboard);
         final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
         final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
-        mLeftPadding = key0.getX();
         mHorizontalStep = Math.abs(key1.getX() - key0.getX());
         mVerticalStep = key0.getHeight() + mVerticalGap;
         mColumnsNum = mBaseWidth / mHorizontalStep;
         mMaxKeyCount = maxKeyCount;
-        mIsRecents = categoryId == EmojiKeyboardView.CATEGORY_ID_RECENTS;
+        mIsRecents = categoryId == EmojiPalettesView.CATEGORY_ID_RECENTS;
         mPrefs = prefs;
     }
 
@@ -74,6 +74,21 @@
         throw new RuntimeException("Can't find template key: code=" + code);
     }
 
+    public void addPendingKey(final Key usedKey) {
+        synchronized (mLock) {
+            mPendingKeys.addLast(usedKey);
+        }
+    }
+
+    public void flushPendingRecentKeys() {
+        synchronized (mLock) {
+            while (!mPendingKeys.isEmpty()) {
+                addKey(mPendingKeys.pollFirst(), true);
+            }
+            saveRecentKeys();
+        }
+    }
+
     public void addKeyFirst(final Key usedKey) {
         addKey(usedKey, true);
         if (mIsRecents) {
@@ -89,7 +104,7 @@
         if (usedKey == null) {
             return;
         }
-        synchronized (mGridKeys) {
+        synchronized (mLock) {
             mCachedGridKeys = null;
             final GridKey key = new GridKey(usedKey);
             while (mGridKeys.remove(key)) {
@@ -105,9 +120,11 @@
             }
             int index = 0;
             for (final GridKey gridKey : mGridKeys) {
-                final int keyX = getKeyX(index);
-                final int keyY = getKeyY(index);
-                gridKey.updateCorrdinates(keyX, keyY);
+                final int keyX0 = getKeyX0(index);
+                final int keyY0 = getKeyY0(index);
+                final int keyX1 = getKeyX1(index);
+                final int keyY1 = getKeyY1(index);
+                gridKey.updateCorrdinates(keyX0, keyY0, keyX1, keyY1);
                 index++;
             }
         }
@@ -155,19 +172,29 @@
         }
     }
 
-    private int getKeyX(final int index) {
+    private int getKeyX0(final int index) {
         final int column = index % mColumnsNum;
-        return column * mHorizontalStep + mLeftPadding;
+        return column * mHorizontalStep;
     }
 
-    private int getKeyY(final int index) {
+    private int getKeyX1(final int index) {
+        final int column = index % mColumnsNum + 1;
+        return column * mHorizontalStep;
+    }
+
+    private int getKeyY0(final int index) {
         final int row = index / mColumnsNum;
-        return row * mVerticalStep + mTopPadding;
+        return row * mVerticalStep;
+    }
+
+    private int getKeyY1(final int index) {
+        final int row = index / mColumnsNum + 1;
+        return row * mVerticalStep;
     }
 
     @Override
     public Key[] getKeys() {
-        synchronized (mGridKeys) {
+        synchronized (mLock) {
             if (mCachedGridKeys != null) {
                 return mCachedGridKeys;
             }
@@ -190,10 +217,10 @@
             super(originalKey);
         }
 
-        public void updateCorrdinates(final int x, final int y) {
-            mCurrentX = x;
-            mCurrentY = y;
-            getHitBox().set(x, y, x + getWidth(), y + getHeight());
+        public void updateCorrdinates(final int x0, final int y0, final int x1, final int y1) {
+            mCurrentX = x0;
+            mCurrentY = y0;
+            getHitBox().set(x0, y0, x1, y1);
         }
 
         @Override
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
index 1716fa0..b528b69 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.content.res.ColorStateList;
 import android.graphics.Typeface;
 
 import com.android.inputmethod.latin.utils.ResourceUtils;
@@ -32,7 +33,7 @@
     public int mHintLabelSize;
     public int mPreviewTextSize;
 
-    public int mTextColor;
+    public ColorStateList mTextColorStateList;
     public int mTextInactivatedColor;
     public int mTextShadowColor;
     public int mHintLetterColor;
@@ -57,7 +58,7 @@
         mHintLabelSize = copyFrom.mHintLabelSize;
         mPreviewTextSize = copyFrom.mPreviewTextSize;
 
-        mTextColor = copyFrom.mTextColor;
+        mTextColorStateList = copyFrom.mTextColorStateList;
         mTextInactivatedColor = copyFrom.mTextInactivatedColor;
         mTextShadowColor = copyFrom.mTextShadowColor;
         mHintLetterColor = copyFrom.mHintLetterColor;
@@ -89,8 +90,8 @@
                 attr.mShiftedLetterHintRatio, mShiftedLetterHintSize);
         mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize);
         mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize);
-
-        mTextColor = selectColor(attr.mTextColor, mTextColor);
+        mTextColorStateList =
+                attr.mTextColorStateList != null ? attr.mTextColorStateList : mTextColorStateList;
         mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
         mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
         mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
index 7a2622c..8bdad36 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Typeface;
 import android.util.SparseIntArray;
@@ -37,7 +38,7 @@
     public final float mHintLabelRatio;
     public final float mPreviewTextRatio;
 
-    public final int mTextColor;
+    public final ColorStateList mTextColorStateList;
     public final int mTextInactivatedColor;
     public final int mTextShadowColor;
     public final int mHintLetterColor;
@@ -115,7 +116,7 @@
         mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
                 R.styleable.Keyboard_Key_keyPreviewTextRatio);
 
-        mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
+        mTextColorStateList = keyAttr.getColorStateList(R.styleable.Keyboard_Key_keyTextColor);
         mTextInactivatedColor = keyAttr.getColor(
                 R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
         mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 22f7a83..c1ae656 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -20,6 +20,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.TypedValue;
@@ -436,17 +437,24 @@
                 final String label;
                 final int code;
                 final String outputText;
+                final int supportedMinSdkVersion;
                 if (codesArrayId != 0) {
                     final String codeArraySpec = array[i];
                     label = CodesArrayParser.parseLabel(codeArraySpec);
                     code = CodesArrayParser.parseCode(codeArraySpec);
                     outputText = CodesArrayParser.parseOutputText(codeArraySpec);
+                    supportedMinSdkVersion =
+                            CodesArrayParser.getMinSupportSdkVersion(codeArraySpec);
                 } else {
                     final String textArraySpec = array[i];
                     // TODO: Utilize KeySpecParser or write more generic TextsArrayParser.
                     label = textArraySpec;
                     code = Constants.CODE_OUTPUT_TEXT;
                     outputText = textArraySpec + (char)Constants.CODE_SPACE;
+                    supportedMinSdkVersion = 0;
+                }
+                if (Build.VERSION.SDK_INT < supportedMinSdkVersion) {
+                    continue;
                 }
                 final int x = (int)row.getKeyX(null);
                 final int y = row.getKeyY();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 67553fb..bcb80b4 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -209,8 +209,8 @@
         /* 104 */ "keylabel_for_tablet_comma",
         /* 105 */ "keyhintlabel_for_tablet_comma",
         /* 106 */ "more_keys_for_tablet_comma",
-        /* 107 */ "keyhintlabel_for_tablet_period",
-        /* 108 */ "more_keys_for_tablet_period",
+        /* 107 */ "keyhintlabel_for_period",
+        /* 108 */ "more_keys_for_period",
         /* 109 */ "keylabel_for_apostrophe",
         /* 110 */ "keyhintlabel_for_apostrophe",
         /* 111 */ "more_keys_for_apostrophe",
@@ -251,6 +251,7 @@
         /* 146 */ "more_keys_for_single_quote",
         /* 147 */ "more_keys_for_double_quote",
         /* 148 */ "more_keys_for_tablet_double_quote",
+        /* 149 */ "emoji_key_as_more_key",
     };
 
     private static final String EMPTY = "";
@@ -439,6 +440,7 @@
         /* 146 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes",
         /* 147 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes",
         /* 148 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes",
+        /* 149 */ "!icon/emoji_key|!code/key_emoji",
     };
 
     /* Language af: Afrikaans */
@@ -2893,33 +2895,69 @@
 
     /* Language sv: Swedish */
     private static final String[] LANGUAGE_sv = {
-        /* 0 */ null,
+        // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+        // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+        // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+        // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+        // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+        /* 0 */ "\u00E1,\u00E0,\u00E2,\u0105,\u00E3",
         // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
         // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
         // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
         // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
         // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
         /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119",
-        /* 2 */ null,
-        // U+0153: "œ" LATIN SMALL LIGATURE OE
-        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
-        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+        // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+        // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+        // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+        /* 2 */ "\u00ED,\u00EC,\u00EE,\u00EF",
         // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+        // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+        // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
         // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
         // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
-        /* 3 */ "\u0153,\u00F4,\u00F2,\u00F3,\u00F5,\u014D",
+        /* 3 */ "\u00F3,\u00F2,\u00F4,\u00F5,\u014D",
         // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
-        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
-        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
         // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+        // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+        // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
         // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
-        /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B",
-        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        /* 4 */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B",
         // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
         // U+0161: "š" LATIN SMALL LETTER S WITH CARON
-        /* 5 */ "\u00DF,\u015B,\u0161",
-        /* 6~ */
-        null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+        // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+        // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+        /* 5 */ "\u015B,\u0161,\u015F,\u00DF",
+        // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+        // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+        // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+        /* 6 */ "\u0144,\u00F1,\u0148",
+        // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+        // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+        // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+        /* 7 */ "\u00E7,\u0107,\u010D",
+        // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+        // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+        // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+        /* 8 */ "\u00FD,\u00FF,\u00FC",
+        // U+00F0: "ð" LATIN SMALL LETTER ETH
+        // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+        /* 9 */ "\u00F0,\u010F",
+        // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+        /* 10 */ "\u0159",
+        // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+        // U+00FE: "þ" LATIN SMALL LETTER THORN
+        /* 11 */ "\u0165,\u00FE",
+        // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+        // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+        // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+        /* 12 */ "\u017A,\u017E,\u017C",
+        /* 13 */ null,
+        // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+        /* 14 */ "\u0142",
+        /* 15~ */
+        null, null, null, null, null,
         /* ~19 */
         // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
         /* 20 */ "\u00E5",
@@ -2928,7 +2966,8 @@
         // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
         /* 22 */ "\u00E4",
         // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
-        /* 23 */ "\u00F8",
+        // U+0153: "œ" LATIN SMALL LIGATURE OE
+        /* 23 */ "\u00F8,\u0153",
         // U+00E6: "æ" LATIN SMALL LETTER AE
         /* 24 */ "\u00E6",
         /* 25~ */
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
index b8ee976..9cf68d4 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
@@ -30,8 +30,9 @@
 import com.android.inputmethod.latin.R;
 
 /**
- * This is an extended {@link KeyboardView} class that hosts a scroll keyboard.
+ * This is an extended {@link KeyboardView} class that hosts a vertical scroll keyboard.
  * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support.
+ * TODO: Vertical scroll capability should be removed from this class because it's no longer used.
  */
 // TODO: Implement key popup preview.
 public final class ScrollKeyboardView extends KeyboardView implements
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
index 4a0ce37..463d093 100644
--- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -41,8 +41,17 @@
 
     abstract public void clear();
 
+    /**
+     * Add a unigram with an optional shortcut to the dictionary.
+     * @param word The word to add.
+     * @param shortcutTarget A shortcut target for this word, or null if none.
+     * @param frequency The frequency for this unigram.
+     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
+     *   if shortcutTarget is null.
+     * @param isNotAWord true if this is not a word, i.e. shortcut only.
+     */
     abstract public void addUnigramWord(final String word, final String shortcutTarget,
-            final int frequency, final boolean isNotAWord);
+            final int frequency, final int shortcutFreq, final boolean isNotAWord);
 
     // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve.
     abstract public void addBigramWords(final String word0, final String word1,
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 29c6c04..fd29698 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -44,14 +44,18 @@
     private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
     // Must be equal to MAX_RESULTS in native/jni/src/defines.h
     private static final int MAX_RESULTS = 18;
-    // Required space count for auto commit.
-    // TODO: Remove this heuristic.
-    private static final int SPACE_COUNT_FOR_AUTO_COMMIT = 3;
+    // The cutoff returned by native for auto-commit confidence.
+    // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h
+    private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000;
 
     @UsedForTesting
     public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT";
     @UsedForTesting
     public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
+    @UsedForTesting
+    public static final String MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
+    @UsedForTesting
+    public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
 
     private long mNativeDict;
     private final Locale mLocale;
@@ -62,7 +66,8 @@
     private final int[] mSpaceIndices = new int[MAX_RESULTS];
     private final int[] mOutputScores = new int[MAX_RESULTS];
     private final int[] mOutputTypes = new int[MAX_RESULTS];
-    private final int[] mOutputAutoCommitFirstWordConfidence = new int[MAX_RESULTS];
+    // Only one result is ever used
+    private final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
 
     private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
 
@@ -269,18 +274,11 @@
         return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1);
     }
 
-    private void runGCIfRequired() {
-        if (needsToRunGC(true /* mindsBlockByGC */)) {
-            flushWithGC();
-        }
-    }
-
     // Add a unigram entry to binary dictionary in native code.
     public void addUnigramWord(final String word, final int probability) {
         if (TextUtils.isEmpty(word)) {
             return;
         }
-        runGCIfRequired();
         final int[] codePoints = StringUtils.toCodePointArray(word);
         addUnigramWordNative(mNativeDict, codePoints, probability);
     }
@@ -290,7 +288,6 @@
         if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
             return;
         }
-        runGCIfRequired();
         final int[] codePoints0 = StringUtils.toCodePointArray(word0);
         final int[] codePoints1 = StringUtils.toCodePointArray(word1);
         addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability);
@@ -301,7 +298,6 @@
         if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
             return;
         }
-        runGCIfRequired();
         final int[] codePoints0 = StringUtils.toCodePointArray(word0);
         final int[] codePoints1 = StringUtils.toCodePointArray(word1);
         removeBigramWordsNative(mNativeDict, codePoints0, codePoints1);
@@ -351,18 +347,7 @@
 
     @Override
     public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
-        // TODO: actually use the confidence rather than use this completely broken heuristic
-        final String word = candidate.mWord;
-        final int length = word.length();
-        int remainingSpaces = SPACE_COUNT_FOR_AUTO_COMMIT;
-        for (int i = 0; i < length; ++i) {
-            // This is okay because no low-surrogate and no high-surrogate can ever match the
-            // space character, so we don't need to take care of iterating on code points.
-            if (Constants.CODE_SPACE == word.charAt(i)) {
-                if (0 >= --remainingSpaces) return true;
-            }
-        }
-        return false;
+        return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT;
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index ffeb927..47891c6 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -127,7 +127,7 @@
             if (DEBUG) {
                 Log.d(TAG, "loadAccountVocabulary: " + word);
             }
-            super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
+            super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */,
                     false /* isNotAWord */);
         }
     }
@@ -213,7 +213,7 @@
                         Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord);
                     }
                     super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
-                            false /* isNotAWord */);
+                            0 /* shortcutFreq */, false /* isNotAWord */);
                     if (!TextUtils.isEmpty(prevWord)) {
                         if (mUseFirstLastBigrams) {
                             super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
index 84abfa6..3df2a2b 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -62,13 +62,13 @@
     // considering performance regression.
     @Override
     public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
-            final boolean isNotAWord) {
+            final int shortcutFreq, final boolean isNotAWord) {
         if (shortcutTarget == null) {
             mFusionDictionary.add(word, frequency, null, isNotAWord);
         } else {
             // TODO: Do this in the subclass, with this class taking an arraylist.
             final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
-            shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
+            shortcutTargets.add(new WeightedString(shortcutTarget, shortcutFreq));
             mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 2d1ca51..eb8650e 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -249,6 +249,9 @@
                     final File file = new File(mContext.getFilesDir(), mFilename);
                     BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(),
                             DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap());
+                    mBinaryDictionary = new BinaryDictionary(
+                            file.getAbsolutePath(), 0 /* offset */, file.length(),
+                            true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
                 } else {
                     mDictionaryWriter.clear();
                 }
@@ -258,10 +261,16 @@
 
     /**
      * Adds a word unigram to the dictionary. Used for loading a dictionary.
+     * @param word The word to add.
+     * @param shortcutTarget A shortcut target for this word, or null if none.
+     * @param frequency The frequency for this unigram.
+     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
+     *   if shortcutTarget is null.
+     * @param isNotAWord true if this is not a word, i.e. shortcut only.
      */
     protected void addWord(final String word, final String shortcutTarget,
-            final int frequency, final boolean isNotAWord) {
-        mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+            final int frequency, final int shortcutFreq, final boolean isNotAWord) {
+        mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, isNotAWord);
     }
 
     /**
@@ -274,23 +283,57 @@
     }
 
     /**
+     * Check whether GC is needed and run GC if required.
+     */
+    protected void runGCIfRequired(final boolean mindsBlockByGC) {
+        if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return;
+        getExecutor(mFilename).execute(new Runnable() {
+            @Override
+            public void run() {
+                runGCIfRequiredInternalLocked(mindsBlockByGC);
+            }
+        });
+    }
+
+    private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) {
+        if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return;
+        // Calls to needsToRunGC() need to be serialized.
+        if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) {
+            if (setIsRegeneratingIfNotRegenerating()) {
+                // Run GC after currently existing time sensitive operations.
+                getExecutor(mFilename).executePrioritized(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            mBinaryDictionary.flushWithGC();
+                        } finally {
+                            mFilenameDictionaryUpdateController.mIsRegenerating.set(false);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+    /**
      * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry.
      */
     protected void addWordDynamically(final String word, final String shortcutTarget,
-            final int frequency, final boolean isNotAWord) {
+            final int frequency, final int shortcutFreq, final boolean isNotAWord) {
         if (!mIsUpdatable) {
             Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
             return;
         }
-
         getExecutor(mFilename).execute(new Runnable() {
             @Override
             public void run() {
                 if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+                    runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
                     mBinaryDictionary.addUnigramWord(word, frequency);
                 } else {
                     // TODO: Remove.
-                    mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+                    mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq,
+                            isNotAWord);
                 }
             }
         });
@@ -306,11 +349,11 @@
                     + mFilename);
             return;
         }
-
         getExecutor(mFilename).execute(new Runnable() {
             @Override
             public void run() {
                 if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+                    runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
                     mBinaryDictionary.addBigramWords(word0, word1, frequency);
                 } else {
                     // TODO: Remove.
@@ -330,11 +373,11 @@
                     + mFilename);
             return;
         }
-
         getExecutor(mFilename).execute(new Runnable() {
             @Override
             public void run() {
                 if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+                    runGCIfRequiredInternalLocked(true /* mindsBlockByGC */);
                     mBinaryDictionary.removeBigramWords(word0, word1);
                 } else {
                     // TODO: Remove.
@@ -461,8 +504,8 @@
         final long length = file.length();
 
         // Build the new binary dictionary
-        final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
-                true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
+        final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0 /* offset */,
+                length, true /* useFullEditDistance */, null, mDictType, mIsUpdatable);
 
         // Ensure all threads accessing the current dictionary have finished before
         // swapping in the new one.
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index d491f98..95c9bca 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -156,15 +156,36 @@
         return Constants.DICTIONARY_MAX_WORD_LENGTH;
     }
 
-    public void addWord(final String word, final String shortcutTarget, final int frequency) {
+    /**
+     * Add a word with an optional shortcut to the dictionary.
+     * @param word The word to add.
+     * @param shortcutTarget A shortcut target for this word, or null if none.
+     * @param frequency The frequency for this unigram.
+     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
+     *   if shortcutTarget is null.
+     */
+    public void addWord(final String word, final String shortcutTarget, final int frequency,
+            final int shortcutFreq) {
         if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
             return;
         }
-        addWordRec(mRoots, word, 0, shortcutTarget, frequency, null);
+        addWordRec(mRoots, word, 0, shortcutTarget, frequency, shortcutFreq, null);
     }
 
+    /**
+     * Add a word, recursively searching for its correct place in the trie tree.
+     * @param children The node to recursively search for addition. Initially, the root of the tree.
+     * @param word The word to add.
+     * @param depth The current depth in the tree.
+     * @param shortcutTarget A shortcut target for this word, or null if none.
+     * @param frequency The frequency for this unigram.
+     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
+     *   if shortcutTarget is null.
+     * @param parentNode The parent node, for up linking. Initially null, as the root has no parent.
+     */
     private void addWordRec(final NodeArray children, final String word, final int depth,
-            final String shortcutTarget, final int frequency, final Node parentNode) {
+            final String shortcutTarget, final int frequency, final int shortcutFreq,
+            final Node parentNode) {
         final int wordLength = word.length();
         if (wordLength <= depth) return;
         final char c = word.charAt(depth);
@@ -204,7 +225,8 @@
         if (childNode.mChildren == null) {
             childNode.mChildren = new NodeArray();
         }
-        addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, childNode);
+        addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, shortcutFreq,
+                childNode);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 96e16de..ccdbd0d 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -77,6 +77,7 @@
 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
 import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
 import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
 import com.android.inputmethod.latin.personalization.PersonalizationHelper;
@@ -567,6 +568,8 @@
         newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
         registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
 
+        DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this);
+
         mInputUpdater = new InputUpdater(this);
     }
 
@@ -602,8 +605,24 @@
     }
 
     private void initSuggest() {
-        final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        final String localeStr = subtypeLocale.toString();
+        final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+        final String switcherLocaleStr = switcherSubtypeLocale.toString();
+        final Locale subtypeLocale;
+        final String localeStr;
+        if (TextUtils.isEmpty(switcherLocaleStr)) {
+            // This happens in very rare corner cases - for example, immediately after a switch
+            // to LatinIME has been requested, about a frame later another switch happens. In this
+            // case, we are about to go down but we still don't know it, however the system tells
+            // us there is no current subtype so the locale is the empty string. Take the best
+            // possible guess instead -- it's bound to have no consequences, and we have no way
+            // of knowing anyway.
+            Log.e(TAG, "System is reporting no current subtype.");
+            subtypeLocale = getResources().getConfiguration().locale;
+            localeStr = subtypeLocale.toString();
+        } else {
+            subtypeLocale = switcherSubtypeLocale;
+            localeStr = switcherLocaleStr;
+        }
 
         final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
                 this /* SuggestInitializationListener */);
@@ -789,6 +808,7 @@
     @SuppressWarnings("deprecation")
     private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
         super.onStartInputView(editorInfo, restarting);
+        mRichImm.clearSubtypeCaches();
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
         // If we are starting input in a different text field from before, we'll have to reload
@@ -884,12 +904,17 @@
         // Sometimes, while rotating, for some reason the framework tells the app we are not
         // connected to it and that means we can't refresh the cache. In this case, schedule a
         // refresh later.
+        final boolean canReachInputConnection;
         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
                 false /* shouldFinishComposition */)) {
             // We try resetting the caches up to 5 times before giving up.
             mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
+            canReachInputConnection = false;
         } else {
-            if (isDifferentTextField) mHandler.postResumeSuggestions();
+            if (isDifferentTextField) {
+                mHandler.postResumeSuggestions();
+            }
+            canReachInputConnection = true;
         }
 
         if (isDifferentTextField) {
@@ -902,6 +927,11 @@
             }
 
             switcher.loadKeyboard(editorInfo, currentSettingsValues);
+            if (!canReachInputConnection) {
+                // If we can't reach the input connection, we will call loadKeyboard again later,
+                // so we need to save its state now. The call will be done in #retryResetCaches.
+                switcher.saveKeyboardState();
+            }
         } else if (restarting) {
             // TODO: Come up with a more comprehensive way to reset the keyboard layout when
             // a keyboard layout set doesn't get reloaded in this method.
@@ -1020,17 +1050,12 @@
     private void onFinishInputViewInternal(final boolean finishingInput) {
         super.onFinishInputView(finishingInput);
         mKeyboardSwitcher.onFinishInputView();
-        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
-        if (mainKeyboardView != null) {
-            mainKeyboardView.cancelAllOngoingEvents();
-            mainKeyboardView.deallocateMemory();
-        }
+        mKeyboardSwitcher.deallocateMemory();
         // Remove pending messages related to update suggestions
         mHandler.cancelUpdateSuggestionStrip();
         // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
         if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
         resetComposingState(true /* alsoResetLastComposedWord */);
-        mRichImm.clearSubtypeCaches();
         // Notify ResearchLogger
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
@@ -1383,14 +1408,15 @@
     // Called from the KeyboardSwitcher which needs to know auto caps state to display
     // the right layout.
     public int getCurrentAutoCapsState() {
-        if (!mSettings.getCurrent().mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+        final SettingsValues currentSettingsValues = mSettings.getCurrent();
+        if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
 
         final EditorInfo ei = getCurrentInputEditorInfo();
         if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
         final int inputType = ei.inputType;
         // Warning: this depends on mSpaceState, which may not be the most current value. If
         // mSpaceState gets updated later, whoever called this may need to be told about it.
-        return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(),
+        return mConnection.getCursorCapsMode(inputType, currentSettingsValues,
                 SPACE_STATE_PHANTOM == mSpaceState);
     }
 
@@ -1431,18 +1457,30 @@
     }
 
     private boolean maybeDoubleSpacePeriod() {
-        final SettingsValues settingsValues = mSettings.getCurrent();
-        if (!settingsValues.mCorrectionEnabled) return false;
-        if (!settingsValues.mUseDoubleSpacePeriod) return false;
+        final SettingsValues currentSettingsValues = mSettings.getCurrent();
+        if (!currentSettingsValues.mCorrectionEnabled) return false;
+        if (!currentSettingsValues.mUseDoubleSpacePeriod) return false;
         if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
-        final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
-        if (lastThree != null && lastThree.length() == 3
-                && canBeFollowedByDoubleSpacePeriod(lastThree.charAt(0))
-                && lastThree.charAt(1) == Constants.CODE_SPACE
-                && lastThree.charAt(2) == Constants.CODE_SPACE) {
+        // We only do this when we see two spaces and an accepted code point before the cursor.
+        // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
+        final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
+        if (null == lastThree) return false;
+        final int length = lastThree.length();
+        if (length < 3) return false;
+        if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
+        if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
+        // We know there are spaces in pos -1 and -2, and we have at least three chars.
+        // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
+        // so this is fine.
+        final int firstCodePoint =
+                Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
+                        Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
+        if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
             mHandler.cancelDoubleSpacePeriodTimer();
             mConnection.deleteSurroundingText(2, 0);
-            final String textToInsert = ". ";
+            final String textToInsert = new String(
+                    new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE },
+                    0, 2);
             mConnection.commitText(textToInsert, 1);
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
@@ -1464,7 +1502,8 @@
                 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
                 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
                 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
-                || codePoint == Constants.CODE_PLUS;
+                || codePoint == Constants.CODE_PLUS
+                || Character.getType(codePoint) == Character.OTHER_SYMBOL;
     }
 
     // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
@@ -2925,10 +2964,13 @@
         if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) {
             if (0 < remainingTries) {
                 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
+                return;
             }
-            return;
+            // If remainingTries is 0, we should stop waiting for new tries, but it's still
+            // better to load the keyboard (less things will be broken).
         }
         tryFixLyingCursorPosition();
+        mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
         if (tryResumeSuggestions) mHandler.postResumeSuggestions();
     }
 
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 8580a6e..e43cab5 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -245,11 +245,11 @@
      * American English, it's just the most common set of rules for English).
      *
      * @param inputType a mask of the caps modes to test for.
-     * @param locale what language should be considered.
+     * @param settingsValues the values of the settings to use for locale and separators.
      * @param hasSpaceBefore if we should consider there should be a space after the string.
      * @return the caps modes that should be on as a set of bits
      */
-    public int getCursorCapsMode(final int inputType, final Locale locale,
+    public int getCursorCapsMode(final int inputType, final SettingsValues settingsValues,
             final boolean hasSpaceBefore) {
         mIC = mParent.getCurrentInputConnection();
         if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
@@ -277,8 +277,8 @@
         }
         // This never calls InputConnection#getCapsMode - in fact, it's a static method that
         // never blocks or initiates IPC.
-        return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale,
-                hasSpaceBefore);
+        return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType,
+                settingsValues, hasSpaceBefore);
     }
 
     public int getCodePointBeforeCursor() {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 9fd1f53..c270d47 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -286,14 +286,16 @@
         // the word *would* have been auto-corrected.
         if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
                 || suggestionsSet.isEmpty() || wordComposer.hasDigits()
-                || wordComposer.isMostlyCaps() || wordComposer.isResumed()
-                || !hasMainDictionary()) {
+                || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary()
+                || SuggestedWordInfo.KIND_SHORTCUT == suggestionsSet.first().mKind) {
             // If we don't have a main dictionary, we never want to auto-correct. The reason for
             // this is, the user may have a contact whose name happens to match a valid word in
             // their language, and it will unexpectedly auto-correct. For example, if the user
             // types in English with no dictionary and has a "Will" in their contact list, "will"
             // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
             // auto-correct.
+            // Also, shortcuts should never auto-correct unless they are whitelist entries.
+            // TODO: we may want to have shortcut-only entries auto-correct in the future.
             hasAutoCorrection = false;
         } else {
             hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 864a173..15b3d8d 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -47,6 +47,9 @@
     private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
     private static final int HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY = 250;
     private static final int LATINIME_DEFAULT_USER_DICTIONARY_FREQUENCY = 160;
+    // Shortcut frequency is 0~15, with 15 = whitelist. We don't want user dictionary entries
+    // to auto-correct, so we set this to the highest frequency that won't, i.e. 14.
+    private static final int USER_DICT_SHORTCUT_FREQUENCY = 14;
 
     // TODO: use Words.SHORTCUT when we target JellyBean or above
     final static String SHORTCUT = "shortcut";
@@ -243,10 +246,12 @@
                 final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency);
                 // Safeguard against adding really long words.
                 if (word.length() < MAX_WORD_LENGTH) {
-                    super.addWord(word, null, adjustedFrequency, false /* isNotAWord */);
+                    super.addWord(word, null, adjustedFrequency, 0 /* shortcutFreq */,
+                            false /* isNotAWord */);
                 }
                 if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) {
-                    super.addWord(shortcut, word, adjustedFrequency, true /* isNotAWord */);
+                    super.addWord(shortcut, word, adjustedFrequency, USER_DICT_SHORTCUT_FREQUENCY,
+                            true /* isNotAWord */);
                 }
                 cursor.moveToNext();
             }
diff --git a/java/src/com/android/inputmethod/latin/about/AboutPreferences.java b/java/src/com/android/inputmethod/latin/about/AboutPreferences.java
new file mode 100644
index 0000000..f60b189
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/about/AboutPreferences.java
@@ -0,0 +1,28 @@
+/*
+ * 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.about;
+
+import android.app.Fragment;
+
+/**
+ * Dummy class of AboutPreferences. Never use this.
+ */
+public final class AboutPreferences extends Fragment {
+    private AboutPreferences() {
+        // Prevents this from being instantiated
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
new file mode 100644
index 0000000..9f7f502
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java
@@ -0,0 +1,206 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
+import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeMap;
+
+/**
+ * A base class of the binary dictionary decoder.
+ */
+public abstract class AbstractDictDecoder implements DictDecoder {
+    protected FileHeader readHeader(final DictBuffer dictBuffer)
+            throws IOException, UnsupportedFormatException {
+        if (dictBuffer == null) {
+            openDictBuffer();
+        }
+
+        final int version = HeaderReader.readVersion(dictBuffer);
+        if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
+                || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
+          throw new UnsupportedFormatException("Unsupported version : " + version);
+        }
+        // TODO: Remove this field.
+        final int optionsFlags = HeaderReader.readOptionFlags(dictBuffer);
+
+        final int headerSize = HeaderReader.readHeaderSize(dictBuffer);
+
+        if (headerSize < 0) {
+            throw new UnsupportedFormatException("header size can't be negative.");
+        }
+
+        final HashMap<String, String> attributes = HeaderReader.readAttributes(dictBuffer,
+                headerSize);
+
+        final FileHeader header = new FileHeader(headerSize,
+                new FusionDictionary.DictionaryOptions(attributes,
+                        0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
+                        0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
+                        new FormatOptions(version,
+                                0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE)));
+        return header;
+    }
+
+    @Override @UsedForTesting
+    public int getTerminalPosition(final String word)
+            throws IOException, UnsupportedFormatException {
+        if (!isDictBufferOpen()) {
+            openDictBuffer();
+        }
+        return BinaryDictIOUtils.getTerminalPosition(this, word);
+    }
+
+    @Override @UsedForTesting
+    public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words,
+            final TreeMap<Integer, Integer> frequencies,
+            final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams)
+            throws IOException, UnsupportedFormatException {
+        if (!isDictBufferOpen()) {
+            openDictBuffer();
+        }
+        BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams);
+    }
+
+    /**
+     * A utility class for reading a file header.
+     */
+    protected static class HeaderReader {
+        protected static int readVersion(final DictBuffer dictBuffer)
+                throws IOException, UnsupportedFormatException {
+            return BinaryDictDecoderUtils.checkFormatVersion(dictBuffer);
+        }
+
+        protected static int readOptionFlags(final DictBuffer dictBuffer) {
+            return dictBuffer.readUnsignedShort();
+        }
+
+        protected static int readHeaderSize(final DictBuffer dictBuffer) {
+            return dictBuffer.readInt();
+        }
+
+        protected static HashMap<String, String> readAttributes(final DictBuffer dictBuffer,
+                final int headerSize) {
+            final HashMap<String, String> attributes = new HashMap<String, String>();
+            while (dictBuffer.position() < headerSize) {
+                // We can avoid an infinite loop here since dictBuffer.position() is always
+                // increased by calling CharEncoding.readString.
+                final String key = CharEncoding.readString(dictBuffer);
+                final String value = CharEncoding.readString(dictBuffer);
+                attributes.put(key, value);
+            }
+            dictBuffer.position(headerSize);
+            return attributes;
+        }
+    }
+
+    /**
+     * A utility class for reading a PtNode.
+     */
+    protected static class PtNodeReader {
+        protected static int readPtNodeOptionFlags(final DictBuffer dictBuffer) {
+            return dictBuffer.readUnsignedByte();
+        }
+
+        protected static int readParentAddress(final DictBuffer dictBuffer,
+                final FormatOptions formatOptions) {
+            if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+                return BinaryDictDecoderUtils.readSInt24(dictBuffer);
+            } else {
+                return FormatSpec.NO_PARENT_ADDRESS;
+            }
+        }
+
+        protected static int readChildrenAddress(final DictBuffer dictBuffer, final int optionFlags,
+                final FormatOptions formatOptions) {
+            if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
+                final int address = BinaryDictDecoderUtils.readSInt24(dictBuffer);
+                if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
+                return address;
+            } else {
+                switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
+                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
+                        return dictBuffer.readUnsignedByte();
+                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES:
+                        return dictBuffer.readUnsignedShort();
+                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES:
+                        return dictBuffer.readUnsignedInt24();
+                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS:
+                    default:
+                        return FormatSpec.NO_CHILDREN_ADDRESS;
+                }
+            }
+        }
+
+        // Reads shortcuts and returns the read length.
+        protected static int readShortcut(final DictBuffer dictBuffer,
+                final ArrayList<WeightedString> shortcutTargets) {
+            final int pointerBefore = dictBuffer.position();
+            dictBuffer.readUnsignedShort(); // skip the size
+            while (true) {
+                final int targetFlags = dictBuffer.readUnsignedByte();
+                final String word = CharEncoding.readString(dictBuffer);
+                shortcutTargets.add(new WeightedString(word,
+                        targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
+                if (0 == (targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
+            }
+            return dictBuffer.position() - pointerBefore;
+        }
+
+        protected static int readBigramAddresses(final DictBuffer dictBuffer,
+                final ArrayList<PendingAttribute> bigrams, final int baseAddress) {
+            int readLength = 0;
+            int bigramCount = 0;
+            while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+                final int bigramFlags = dictBuffer.readUnsignedByte();
+                ++readLength;
+                final int sign = 0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE)
+                        ? 1 : -1;
+                int bigramAddress = baseAddress + readLength;
+                switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
+                        bigramAddress += sign * dictBuffer.readUnsignedByte();
+                        readLength += 1;
+                        break;
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
+                        bigramAddress += sign * dictBuffer.readUnsignedShort();
+                        readLength += 2;
+                        break;
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
+                        bigramAddress += sign * dictBuffer.readUnsignedInt24();
+                        readLength += 3;
+                        break;
+                    default:
+                        throw new RuntimeException("Has bigrams with no address");
+                }
+                bigrams.add(new PendingAttribute(
+                        bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
+                        bigramAddress));
+                if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
+            }
+            return readLength;
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index 665c7a2..216492b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -23,11 +23,11 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.ArrayList;
@@ -219,14 +219,14 @@
         }
 
         /**
-         * Writes a string with our character format to a ByteArrayOutputStream.
+         * Writes a string with our character format to an OutputStream.
          *
          * This will also write the terminator byte.
          *
-         * @param buffer the ByteArrayOutputStream to write to.
+         * @param buffer the OutputStream to write to.
          * @param word the string to write.
          */
-        static void writeString(final ByteArrayOutputStream buffer, final String word) {
+        static void writeString(final OutputStream buffer, final String word) throws IOException {
             final int length = word.length();
             for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
                 final int codePoint = word.codePointAt(i);
@@ -295,7 +295,6 @@
                 return address;
             }
         }
-        int address;
         switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
             case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
                 return dictBuffer.readUnsignedByte();
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
index af61f29..f761829 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -278,7 +278,6 @@
         // For future reference, the code to remove duplicate is a simple : list.remove(node);
         list.add(ptNodeArray);
         final ArrayList<PtNode> branches = ptNodeArray.mData;
-        final int nodeSize = branches.size();
         for (PtNode ptNode : branches) {
             if (null != ptNode.mChildren) flattenTreeInner(list, ptNode.mChildren);
         }
@@ -384,8 +383,8 @@
                 nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray,
                         nodeSize + size, ptNode.mChildren));
             }
-            nodeSize += getShortcutListSize(ptNode.mShortcutTargets);
             if (formatOptions.mVersion < FormatSpec.FIRST_VERSION_WITH_TERMINAL_ID) {
+                nodeSize += getShortcutListSize(ptNode.mShortcutTargets);
                 if (null != ptNode.mBigrams) {
                     for (WeightedString bigram : ptNode.mBigrams) {
                         final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray,
@@ -427,9 +426,6 @@
                         nodeCountSize + nodeArrayOffset + nodeffset;
                 nodeffset += ptNode.mCachedSize;
             }
-            final int nodeSize = nodeCountSize + nodeffset
-                    + (formatOptions.mSupportsDynamicUpdate
-                            ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
             nodeArrayOffset += nodeArray.mCachedSize;
         }
         return nodeArrayOffset;
@@ -653,8 +649,8 @@
         return flags;
     }
 
-    /* package */ static byte makePtNodeFlags(final PtNode node, final int ptNodeAddress,
-            final int childrenOffset, final FormatOptions formatOptions) {
+    /* package */ static byte makePtNodeFlags(final PtNode node, final int childrenOffset,
+            final FormatOptions formatOptions) {
         return (byte) makePtNodeFlags(node.mChars.length > 1, node.mFrequency >= 0,
                 getByteSize(childrenOffset),
                 node.mShortcutTargets != null && !node.mShortcutTargets.isEmpty(),
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
index a282f59..0f7d2f6 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -288,40 +288,6 @@
         return BinaryDictEncoderUtils.getByteSize(value);
     }
 
-    static void skipPtNode(final DictBuffer dictBuffer, final FormatOptions formatOptions) {
-        final int flags = dictBuffer.readUnsignedByte();
-        BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions);
-        skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
-        BinaryDictDecoderUtils.readChildrenAddress(dictBuffer, flags, formatOptions);
-        if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte();
-        if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) {
-            final int shortcutsSize = dictBuffer.readUnsignedShort();
-            dictBuffer.position(dictBuffer.position() + shortcutsSize
-                    - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
-        }
-        if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) {
-            int bigramCount = 0;
-            while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
-                final int bigramFlags = dictBuffer.readUnsignedByte();
-                switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
-                        dictBuffer.readUnsignedByte();
-                        break;
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
-                        dictBuffer.readUnsignedShort();
-                        break;
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
-                        dictBuffer.readUnsignedInt24();
-                        break;
-                }
-                if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break;
-            }
-            if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
-                throw new RuntimeException("Too many bigrams in a PtNode.");
-            }
-        }
-    }
-
     static void skipString(final DictBuffer dictBuffer,
             final boolean hasMultipleChars) {
         if (hasMultipleChars) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
index 3796a46..3dbeee0 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
@@ -17,11 +17,9 @@
 package com.android.inputmethod.latin.makedict;
 
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 import com.android.inputmethod.latin.utils.ByteArrayDictBuffer;
 
 import java.io.File;
@@ -32,50 +30,17 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.TreeMap;
 
 /**
- * The base class of binary dictionary decoders.
+ * An interface of binary dictionary decoders.
  */
-public abstract class DictDecoder {
-
-    protected FileHeader readHeader(final DictBuffer dictBuffer)
-            throws IOException, UnsupportedFormatException {
-        if (dictBuffer == null) {
-            openDictBuffer();
-        }
-
-        final int version = HeaderReader.readVersion(dictBuffer);
-        if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
-                || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
-            throw new UnsupportedFormatException("Unsupported version : " + version);
-        }
-        // TODO: Remove this field.
-        final int optionsFlags = HeaderReader.readOptionFlags(dictBuffer);
-
-        final int headerSize = HeaderReader.readHeaderSize(dictBuffer);
-
-        if (headerSize < 0) {
-            throw new UnsupportedFormatException("header size can't be negative.");
-        }
-
-        final HashMap<String, String> attributes = HeaderReader.readAttributes(dictBuffer,
-                headerSize);
-
-        final FileHeader header = new FileHeader(headerSize,
-                new FusionDictionary.DictionaryOptions(attributes,
-                        0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG),
-                        0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)),
-                        new FormatOptions(version,
-                                0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE)));
-        return header;
-    }
+public interface DictDecoder {
 
     /**
      * Reads and returns the file header.
      */
-    public abstract FileHeader readHeader() throws IOException, UnsupportedFormatException;
+    public FileHeader readHeader() throws IOException, UnsupportedFormatException;
 
     /**
      * Reads PtNode from nodeAddress.
@@ -83,7 +48,7 @@
      * @param formatOptions the format options.
      * @return PtNodeInfo.
      */
-    public abstract PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions formatOptions);
+    public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions formatOptions);
 
     /**
      * Reads a buffer and returns the memory representation of the dictionary.
@@ -98,7 +63,7 @@
      * @return the created (or merged) dictionary.
      */
     @UsedForTesting
-    public abstract FusionDictionary readDictionaryBinary(final FusionDictionary dict,
+    public FusionDictionary readDictionaryBinary(final FusionDictionary dict,
             final boolean deleteDictIfBroken)
                     throws FileNotFoundException, IOException, UnsupportedFormatException;
 
@@ -113,12 +78,7 @@
      */
     @UsedForTesting
     public int getTerminalPosition(final String word)
-            throws IOException, UnsupportedFormatException {
-        if (!isDictBufferOpen()) {
-            openDictBuffer();
-        }
-        return BinaryDictIOUtils.getTerminalPosition(this, word);
-    }
+            throws IOException, UnsupportedFormatException;
 
     /**
      * Reads unigrams and bigrams from the binary file.
@@ -134,47 +94,42 @@
     public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words,
             final TreeMap<Integer, Integer> frequencies,
             final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams)
-            throws IOException, UnsupportedFormatException {
-        if (!isDictBufferOpen()) {
-            openDictBuffer();
-        }
-        BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams);
-    }
+                throws IOException, UnsupportedFormatException;
 
     /**
      * Sets the position of the buffer to the given value.
      *
      * @param newPos the new position
      */
-    public abstract void setPosition(final int newPos);
+    public void setPosition(final int newPos);
 
     /**
      * Gets the position of the buffer.
      *
      * @return the position
      */
-    public abstract int getPosition();
+    public int getPosition();
 
     /**
      * Reads and returns the PtNode count out of a buffer and forwards the pointer.
      */
-    public abstract int readPtNodeCount();
+    public int readPtNodeCount();
 
     /**
      * Reads the forward link and advances the position.
      *
      * @return true if this method moves the file pointer, false otherwise.
      */
-    public abstract boolean readAndFollowForwardLink();
-    public abstract boolean hasNextPtNodeArray();
+    public boolean readAndFollowForwardLink();
+    public boolean hasNextPtNodeArray();
 
     /**
      * Opens the dictionary file and makes DictBuffer.
      */
     @UsedForTesting
-    public abstract void openDictBuffer() throws FileNotFoundException, IOException;
+    public void openDictBuffer() throws FileNotFoundException, IOException;
     @UsedForTesting
-    public abstract boolean isDictBufferOpen();
+    public boolean isDictBufferOpen();
 
     // Constants for DictionaryBufferFactory.
     public static final int USE_READONLY_BYTEBUFFER = 0x01000000;
@@ -272,123 +227,5 @@
         }
     }
 
-    /**
-     * A utility class for reading a file header.
-     */
-    protected static class HeaderReader {
-        protected static int readVersion(final DictBuffer dictBuffer)
-                throws IOException, UnsupportedFormatException {
-            return BinaryDictDecoderUtils.checkFormatVersion(dictBuffer);
-        }
-
-        protected static int readOptionFlags(final DictBuffer dictBuffer) {
-            return dictBuffer.readUnsignedShort();
-        }
-
-        protected static int readHeaderSize(final DictBuffer dictBuffer) {
-            return dictBuffer.readInt();
-        }
-
-        protected static HashMap<String, String> readAttributes(final DictBuffer dictBuffer,
-                final int headerSize) {
-            final HashMap<String, String> attributes = new HashMap<String, String>();
-            while (dictBuffer.position() < headerSize) {
-                // We can avoid an infinite loop here since dictBuffer.position() is always
-                // increased by calling CharEncoding.readString.
-                final String key = CharEncoding.readString(dictBuffer);
-                final String value = CharEncoding.readString(dictBuffer);
-                attributes.put(key, value);
-            }
-            dictBuffer.position(headerSize);
-            return attributes;
-        }
-    }
-
-    /**
-     * A utility class for reading a PtNode.
-     */
-    protected static class PtNodeReader {
-        protected static int readPtNodeOptionFlags(final DictBuffer dictBuffer) {
-            return dictBuffer.readUnsignedByte();
-        }
-
-        protected static int readParentAddress(final DictBuffer dictBuffer,
-                final FormatOptions formatOptions) {
-            if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
-                return BinaryDictDecoderUtils.readSInt24(dictBuffer);
-            } else {
-                return FormatSpec.NO_PARENT_ADDRESS;
-            }
-        }
-
-        protected static int readChildrenAddress(final DictBuffer dictBuffer, final int optionFlags,
-                final FormatOptions formatOptions) {
-            if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) {
-                final int address = BinaryDictDecoderUtils.readSInt24(dictBuffer);
-                if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
-                return address;
-            } else {
-                switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) {
-                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE:
-                        return dictBuffer.readUnsignedByte();
-                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES:
-                        return dictBuffer.readUnsignedShort();
-                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES:
-                        return dictBuffer.readUnsignedInt24();
-                    case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS:
-                    default:
-                        return FormatSpec.NO_CHILDREN_ADDRESS;
-                }
-            }
-        }
-
-        // Reads shortcuts and returns the read length.
-        protected static int readShortcut(final DictBuffer dictBuffer,
-                final ArrayList<WeightedString> shortcutTargets) {
-            final int pointerBefore = dictBuffer.position();
-            dictBuffer.readUnsignedShort(); // skip the size
-            while (true) {
-                final int targetFlags = dictBuffer.readUnsignedByte();
-                final String word = CharEncoding.readString(dictBuffer);
-                shortcutTargets.add(new WeightedString(word,
-                        targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
-                if (0 == (targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
-            }
-            return dictBuffer.position() - pointerBefore;
-        }
-
-        protected static int readBigramAddresses(final DictBuffer dictBuffer,
-                final ArrayList<PendingAttribute> bigrams, final int baseAddress) {
-            int readLength = 0;
-            int bigramCount = 0;
-            while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
-                final int bigramFlags = dictBuffer.readUnsignedByte();
-                ++readLength;
-                final int sign = 0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE)
-                        ? 1 : -1;
-                int bigramAddress = baseAddress + readLength;
-                switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
-                        bigramAddress += sign * dictBuffer.readUnsignedByte();
-                        readLength += 1;
-                        break;
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
-                        bigramAddress += sign * dictBuffer.readUnsignedShort();
-                        readLength += 2;
-                        break;
-                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
-                        bigramAddress += sign * dictBuffer.readUnsignedInt24();
-                        readLength += 3;
-                        break;
-                    default:
-                        throw new RuntimeException("Has bigrams with no address");
-                }
-                bigrams.add(new PendingAttribute(
-                        bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY,
-                        bigramAddress));
-                if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
-            }
-            return readLength;
-        }
-    }
+    public void skipPtNode(final FormatOptions formatOptions);
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java
new file mode 100644
index 0000000..c4f7ec9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java
@@ -0,0 +1,54 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * An interface of a binary dictionary updater.
+ */
+@UsedForTesting
+public interface DictUpdater extends DictDecoder {
+
+    /**
+     * Deletes the word from the binary dictionary.
+     *
+     * @param word the word to be deleted.
+     */
+    @UsedForTesting
+    public void deleteWord(final String word) throws IOException, UnsupportedFormatException;
+
+    /**
+     * Inserts a word into a binary dictionary.
+     *
+     * @param word the word to be inserted.
+     * @param frequency the frequency of the new word.
+     * @param bigramStrings bigram list, or null if none.
+     * @param shortcuts shortcut list, or null if none.
+     * @param isBlackListEntry whether this should be a blacklist entry.
+     */
+    // TODO: Support batch insertion.
+    @UsedForTesting
+    public void insertWord(final String word, final int frequency,
+            final ArrayList<WeightedString> bigramStrings,
+            final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
+            final boolean isBlackListEntry) throws IOException, UnsupportedFormatException;
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
index 411e265..3362771 100644
--- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -42,44 +42,22 @@
         // This utility class is not publicly instantiable.
     }
 
-    private static int markAsDeleted(final int flags) {
+    /* package */ static int markAsDeleted(final int flags) {
         return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
     }
 
     /**
-     * Delete the word from the binary file.
-     *
-     * @param dictDecoder the dict decoder.
-     * @param word the word we delete
-     * @throws IOException
-     * @throws UnsupportedFormatException
-     */
-    @UsedForTesting
-    public static void deleteWord(final Ver3DictDecoder dictDecoder, final String word)
-            throws IOException, UnsupportedFormatException {
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
-        dictBuffer.position(0);
-        final FileHeader header = dictDecoder.readHeader();
-        final int wordPosition = dictDecoder.getTerminalPosition(word);
-        if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
-
-        dictBuffer.position(wordPosition);
-        final int flags = dictBuffer.readUnsignedByte();
-        dictBuffer.position(wordPosition);
-        dictBuffer.put((byte)markAsDeleted(flags));
-    }
-
-    /**
      * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress.
      *
-     * @param dictBuffer the DictBuffer to write.
+     * @param dictUpdater the DictUpdater to write.
      * @param ptNodeOriginAddress the address of the PtNode.
      * @param newParentAddress the absolute address of the parent.
      * @param formatOptions file format options.
      */
-    private static void updateParentAddress(final DictBuffer dictBuffer,
+    private static void updateParentAddress(final Ver3DictUpdater dictUpdater,
             final int ptNodeOriginAddress, final int newParentAddress,
             final FormatOptions formatOptions) {
+        final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
         final int originalPosition = dictBuffer.position();
         dictBuffer.position(ptNodeOriginAddress);
         if (!formatOptions.mSupportsDynamicUpdate) {
@@ -104,46 +82,45 @@
     /**
      * Update parent addresses in a node array stored at ptNodeOriginAddress.
      *
-     * @param dictBuffer the DictBuffer to be modified.
+     * @param dictUpdater the DictUpdater to be modified.
      * @param ptNodeOriginAddress the address of the node array to update.
      * @param newParentAddress the address to be written.
      * @param formatOptions file format options.
      */
-    private static void updateParentAddresses(final DictBuffer dictBuffer,
+    private static void updateParentAddresses(final Ver3DictUpdater dictUpdater,
             final int ptNodeOriginAddress, final int newParentAddress,
             final FormatOptions formatOptions) {
-        final int originalPosition = dictBuffer.position();
-        dictBuffer.position(ptNodeOriginAddress);
+        final int originalPosition = dictUpdater.getPosition();
+        dictUpdater.setPosition(ptNodeOriginAddress);
         do {
-            final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+            final int count = dictUpdater.readPtNodeCount();
             for (int i = 0; i < count; ++i) {
-                updateParentAddress(dictBuffer, dictBuffer.position(), newParentAddress,
+                updateParentAddress(dictUpdater, dictUpdater.getPosition(), newParentAddress,
                         formatOptions);
-                BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions);
+                dictUpdater.skipPtNode(formatOptions);
             }
-            final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
-            dictBuffer.position(forwardLinkAddress);
-        } while (formatOptions.mSupportsDynamicUpdate
-                && dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
-        dictBuffer.position(originalPosition);
+            if (!dictUpdater.readAndFollowForwardLink()) break;
+            if (dictUpdater.getPosition() == FormatSpec.NO_FORWARD_LINK_ADDRESS) break;
+        } while (formatOptions.mSupportsDynamicUpdate);
+        dictUpdater.setPosition(originalPosition);
     }
 
     /**
      * Update a children address in a PtNode that is addressed by ptNodeOriginAddress.
      *
-     * @param dictBuffer the DictBuffer to write.
+     * @param dictUpdater the DictUpdater to write.
      * @param ptNodeOriginAddress the address of the PtNode.
      * @param newChildrenAddress the absolute address of the child.
      * @param formatOptions file format options.
      */
-    private static void updateChildrenAddress(final DictBuffer dictBuffer,
+    private static void updateChildrenAddress(final Ver3DictUpdater dictUpdater,
             final int ptNodeOriginAddress, final int newChildrenAddress,
             final FormatOptions formatOptions) {
+        final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
         final int originalPosition = dictBuffer.position();
         dictBuffer.position(ptNodeOriginAddress);
         final int flags = dictBuffer.readUnsignedByte();
-        final int parentAddress = BinaryDictDecoderUtils.readParentAddress(dictBuffer,
-                formatOptions);
+        BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions);
         BinaryDictIOUtils.skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
         if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte();
         final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
@@ -156,31 +133,33 @@
      * Helper method to move a PtNode to the tail of the file.
      */
     private static int movePtNode(final OutputStream destination,
-            final DictBuffer dictBuffer, final PtNodeInfo info,
+            final Ver3DictUpdater dictUpdater, final PtNodeInfo info,
             final int nodeArrayOriginAddress, final int oldNodeAddress,
             final FormatOptions formatOptions) throws IOException {
-        updateParentAddress(dictBuffer, oldNodeAddress, dictBuffer.limit() + 1, formatOptions);
+        final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
+        updateParentAddress(dictUpdater, oldNodeAddress, dictBuffer.limit() + 1, formatOptions);
         dictBuffer.position(oldNodeAddress);
         final int currentFlags = dictBuffer.readUnsignedByte();
         dictBuffer.position(oldNodeAddress);
         dictBuffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
                 & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
         int size = FormatSpec.PTNODE_FLAGS_SIZE;
-        updateForwardLink(dictBuffer, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions);
+        updateForwardLink(dictUpdater, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions);
         size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info });
         return size;
     }
 
     @SuppressWarnings("unused")
-    private static void updateForwardLink(final DictBuffer dictBuffer,
+    private static void updateForwardLink(final Ver3DictUpdater dictUpdater,
             final int nodeArrayOriginAddress, final int newNodeArrayAddress,
             final FormatOptions formatOptions) {
-        dictBuffer.position(nodeArrayOriginAddress);
+        final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
+        dictUpdater.setPosition(nodeArrayOriginAddress);
         int jumpCount = 0;
         while (jumpCount++ < MAX_JUMPS) {
-            final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+            final int count = dictUpdater.readPtNodeCount();
             for (int i = 0; i < count; ++i) {
-                BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions);
+                dictUpdater.readPtNode(dictUpdater.getPosition(), formatOptions);
             }
             final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
             if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
@@ -208,7 +187,7 @@
      * @param shortcutTargets the shortcut targets for this PtNode.
      * @param bigrams the bigrams for this PtNode.
      * @param destination the stream representing the tail of the file.
-     * @param dictBuffer the DictBuffer representing the (constant-size) body of the file.
+     * @param dictUpdater the DictUpdater.
      * @param oldPtNodeArrayOrigin the origin of the old PtNode array this PtNode was a part of.
      * @param oldPtNodeOrigin the old origin where this PtNode used to be stored.
      * @param formatOptions format options for this dictionary.
@@ -219,7 +198,7 @@
             final int length, final int flags, final int frequency, final int parentAddress,
             final ArrayList<WeightedString> shortcutTargets,
             final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
-            final DictBuffer dictBuffer, final int oldPtNodeArrayOrigin,
+            final Ver3DictUpdater dictUpdater, final int oldPtNodeArrayOrigin,
             final int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException {
         int size = 0;
         final int newPtNodeOrigin = fileEndAddress + 1;
@@ -232,7 +211,7 @@
                 flags, writtenCharacters, frequency, parentAddress,
                 fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
                 bigrams);
-        movePtNode(destination, dictBuffer, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin,
+        movePtNode(destination, dictUpdater, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin,
                 formatOptions);
         return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
     }
@@ -240,7 +219,7 @@
     /**
      * Insert a word into a binary dictionary.
      *
-     * @param dictDecoder the dict decoder.
+     * @param dictUpdater the dict updater.
      * @param destination a stream to the underlying file, with the pointer at the end of the file.
      * @param word the word to insert.
      * @param frequency the frequency of the new word.
@@ -253,17 +232,17 @@
     // TODO: Support batch insertion.
     // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
     @UsedForTesting
-    public static void insertWord(final Ver3DictDecoder dictDecoder,
+    public static void insertWord(final Ver3DictUpdater dictUpdater,
             final OutputStream destination, final String word, final int frequency,
             final ArrayList<WeightedString> bigramStrings,
             final ArrayList<WeightedString> shortcuts, final boolean isNotAWord,
             final boolean isBlackListEntry)
                     throws IOException, UnsupportedFormatException {
         final ArrayList<PendingAttribute> bigrams = new ArrayList<PendingAttribute>();
-        final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+        final DictBuffer dictBuffer = dictUpdater.getDictBuffer();
         if (bigramStrings != null) {
             for (final WeightedString bigram : bigramStrings) {
-                int position = dictDecoder.getTerminalPosition(bigram.mWord);
+                int position = dictUpdater.getTerminalPosition(bigram.mWord);
                 if (position == FormatSpec.NOT_VALID_WORD) {
                     // TODO: figure out what is the correct thing to do here.
                 } else {
@@ -278,7 +257,7 @@
 
         // find the insert position of the word.
         if (dictBuffer.position() != 0) dictBuffer.position(0);
-        final FileHeader fileHeader = dictDecoder.readHeader();
+        final FileHeader fileHeader = dictUpdater.readHeader();
 
         int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position();
         final int[] codePoints = FusionDictionary.getCodePoints(word);
@@ -293,7 +272,7 @@
 
             for (int i = 0; i < ptNodeCount; ++i) {
                 address = dictBuffer.position();
-                final PtNodeInfo currentInfo = dictDecoder.readPtNode(address,
+                final PtNodeInfo currentInfo = dictUpdater.readPtNode(address,
                         fileHeader.mFormatOptions);
                 final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags,
                         fileHeader.mFormatOptions);
@@ -319,12 +298,12 @@
                                 false /* isBlackListEntry */, fileHeader.mFormatOptions);
                         int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags,
                                 frequency, nodeParentAddress, shortcuts, bigrams, destination,
-                                dictBuffer, nodeOriginAddress, address, fileHeader.mFormatOptions);
+                                dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions);
 
                         final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
                                 currentInfo.mCharacters.length);
                         if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
-                            updateParentAddresses(dictBuffer, currentInfo.mChildrenAddress,
+                            updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress,
                                     newNodeAddress + written + 1, fileHeader.mFormatOptions);
                         }
                         final PtNodeInfo newInfo2 = new PtNodeInfo(
@@ -360,13 +339,13 @@
                                     fileHeader.mFormatOptions);
                             int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p,
                                     prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
-                                    destination, dictBuffer, nodeOriginAddress, address,
+                                    destination, dictUpdater, nodeOriginAddress, address,
                                     fileHeader.mFormatOptions);
 
                             final int[] suffixCharacters = Arrays.copyOfRange(
                                     currentInfo.mCharacters, p, currentInfo.mCharacters.length);
                             if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
-                                updateParentAddresses(dictBuffer, currentInfo.mChildrenAddress,
+                                updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress,
                                         newNodeAddress + written + 1, fileHeader.mFormatOptions);
                             }
                             final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags(
@@ -417,7 +396,7 @@
                                 -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
                                 nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
                                 bigrams);
-                        movePtNode(destination, dictBuffer, newInfo, nodeOriginAddress, address,
+                        movePtNode(destination, dictUpdater, newInfo, nodeOriginAddress, address,
                                 fileHeader.mFormatOptions);
                         return;
                     }
@@ -436,7 +415,7 @@
                          * ab - cd - e
                          */
                         final int newNodeArrayAddress = dictBuffer.limit();
-                        updateChildrenAddress(dictBuffer, address, newNodeArrayAddress,
+                        updateChildrenAddress(dictUpdater, address, newNodeArrayAddress,
                                 fileHeader.mFormatOptions);
                         final int newNodeAddress = newNodeArrayAddress + 1;
                         final boolean hasMultipleChars = (wordLen - wordPos) > 1;
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 9481a8c..5a5d7af 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -266,11 +266,27 @@
     // tat = Terminal Address Table
     static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat";
     static final String BIGRAM_FILE_EXTENSION = ".bigram";
-    static final String BIGRAM_LOOKUP_TABLE_FILE_EXTENSION = ".bigram_lookup";
-    static final String BIGRAM_ADDRESS_TABLE_FILE_EXTENSION = ".bigram_index";
+    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 FREQUENCY_AND_FLAGS_SIZE = 2;
     static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
+
+    // With the English main dictionary as of October 2013, the size of bigram address table is
+    // is 584KB with the block size being 4.
+    // This is 91% of that of full address table.
     static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4;
+    static final int BIGRAM_CONTENT_COUNT = 1;
+    static final int BIGRAM_FREQ_CONTENT_INDEX = 0;
+    static final String BIGRAM_FREQ_CONTENT_ID = "_freq";
+
+    static final int SHORTCUT_CONTENT_COUNT = 1;
+    static final int SHORTCUT_CONTENT_INDEX = 0;
+    // With the English main dictionary as of October 2013, the size of shortcut address table is
+    // 29KB with the block size being 64.
+    // This is only 4.4% of that of full address table.
+    static final int SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64;
+    static final String SHORTCUT_CONTENT_ID = "_shortcut";
 
     static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
     static final int NO_PARENT_ADDRESS = 0;
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index be653fe..3bb218b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -367,10 +367,11 @@
      * Helper method to convert a String to an int array.
      */
     static int[] getCodePoints(final String word) {
-        // TODO: this is a copy-paste of the contents of StringUtils.toCodePointArray,
+        // TODO: this is a copy-paste of the old contents of StringUtils.toCodePointArray,
         // which is not visible from the makedict package. Factor this code.
+        final int length = word.length();
+        if (length <= 0) return new int[] {};
         final char[] characters = word.toCharArray();
-        final int length = characters.length;
         final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
         int codePoint = Character.codePointAt(characters, 0);
         int dsti = 0;
diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java
index 96d057a..7592a0c 100644
--- a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java
+++ b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.makedict;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -37,35 +38,39 @@
     /**
      * mLookupTable is indexed by terminal ID, containing exactly one entry for every mBlockSize
      * terminals.
-     * It contains at index i = j / mBlockSize the index in mContentsTable where the values for
-     * terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized integer array.
+     * It contains at index i = j / mBlockSize the index in each ArrayList in mContentsTables where
+     * the values for terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized
+     * integer array.
      */
     private final ArrayList<Integer> mLookupTable;
-    private final ArrayList<Integer> mContentTable;
+    private final ArrayList<ArrayList<Integer>> mContentTables;
 
     private final int mBlockSize;
+    private final int mContentTableCount;
     public static final int NOT_EXIST = -1;
+    public static final int SIZE_OF_INT_IN_BYTES = 4;
 
     @UsedForTesting
-    public SparseTable(final int initialCapacity, final int blockSize) {
+    public SparseTable(final int initialCapacity, final int blockSize,
+            final int contentTableCount) {
         mBlockSize = blockSize;
         final int lookupTableSize = initialCapacity / mBlockSize
                 + (initialCapacity % mBlockSize > 0 ? 1 : 0);
         mLookupTable = new ArrayList<Integer>(Collections.nCopies(lookupTableSize, NOT_EXIST));
-        mContentTable = new ArrayList<Integer>();
+        mContentTableCount = contentTableCount;
+        mContentTables = CollectionUtils.newArrayList();
+        for (int i = 0; i < mContentTableCount; ++i) {
+            mContentTables.add(new ArrayList<Integer>());
+        }
     }
 
     @UsedForTesting
-    public SparseTable(final int[] lookupTable, final int[] contentTable, final int blockSize) {
+    public SparseTable(final ArrayList<Integer> lookupTable,
+            final ArrayList<ArrayList<Integer>> contentTables, final int blockSize) {
         mBlockSize = blockSize;
-        mLookupTable = new ArrayList<Integer>(lookupTable.length);
-        for (int i = 0; i < lookupTable.length; ++i) {
-            mLookupTable.add(lookupTable[i]);
-        }
-        mContentTable = new ArrayList<Integer>(contentTable.length);
-        for (int i = 0; i < contentTable.length; ++i) {
-            mContentTable.add(contentTable[i]);
-        }
+        mContentTableCount = contentTables.size();
+        mLookupTable = lookupTable;
+        mContentTables = contentTables;
     }
 
     /**
@@ -75,8 +80,8 @@
      * Otherwise, IndexOutOfBoundsException will be raised.
      */
     @UsedForTesting
-    private static void convertByteArrayToIntegerArray(final byte[] byteArray,
-            final ArrayList<Integer> integerArray) {
+    private static ArrayList<Integer> convertByteArrayToIntegerArray(final byte[] byteArray) {
+        final ArrayList<Integer> integerArray = new ArrayList<Integer>(byteArray.length / 4);
         for (int i = 0; i < byteArray.length; i += 4) {
             int value = 0;
             for (int j = i; j < i + 4; ++j) {
@@ -85,39 +90,43 @@
              }
             integerArray.add(value);
         }
+        return integerArray;
     }
 
     @UsedForTesting
-    public SparseTable(final byte[] lookupTable, final byte[] contentTable, final int blockSize) {
-        mBlockSize = blockSize;
-        mLookupTable = new ArrayList<Integer>(lookupTable.length / 4);
-        mContentTable = new ArrayList<Integer>(contentTable.length / 4);
-        convertByteArrayToIntegerArray(lookupTable, mLookupTable);
-        convertByteArrayToIntegerArray(contentTable, mContentTable);
-    }
-
-    @UsedForTesting
-    public int get(final int index) {
-        if (index < 0 || index / mBlockSize >= mLookupTable.size()
-                || mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
+    public int get(final int contentTableIndex, final int index) {
+        if (!contains(index)) {
             return NOT_EXIST;
         }
-        return mContentTable.get(mLookupTable.get(index / mBlockSize) + (index % mBlockSize));
+        return mContentTables.get(contentTableIndex).get(
+                mLookupTable.get(index / mBlockSize) + (index % mBlockSize));
     }
 
     @UsedForTesting
-    public void set(final int index, final int value) {
-        if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
-            mLookupTable.set(index / mBlockSize, mContentTable.size());
-            for (int i = 0; i < mBlockSize; ++i) {
-                mContentTable.add(NOT_EXIST);
-            }
+    public ArrayList<Integer> getAll(final int index) {
+        final ArrayList<Integer> ret = CollectionUtils.newArrayList();
+        for (int i = 0; i < mContentTableCount; ++i) {
+            ret.add(get(i, index));
         }
-        mContentTable.set(mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value);
+        return ret;
     }
 
-    public void remove(final int index) {
-        set(index, NOT_EXIST);
+    @UsedForTesting
+    public void set(final int contentTableIndex, final int index, final int value) {
+        if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
+            mLookupTable.set(index / mBlockSize, mContentTables.get(contentTableIndex).size());
+            for (int i = 0; i < mContentTableCount; ++i) {
+                for (int j = 0; j < mBlockSize; ++j) {
+                    mContentTables.get(i).add(NOT_EXIST);
+                }
+            }
+        }
+        mContentTables.get(contentTableIndex).set(
+                mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value);
+    }
+
+    public void remove(final int indexOfContent, final int index) {
+        set(indexOfContent, index, NOT_EXIST);
     }
 
     @UsedForTesting
@@ -127,7 +136,8 @@
 
     @UsedForTesting
     /* package */ int getContentTableSize() {
-        return mContentTable.size();
+        // This class always has at least one content table.
+        return mContentTables.get(0).size();
     }
 
     @UsedForTesting
@@ -136,36 +146,51 @@
     }
 
     public boolean contains(final int index) {
-        return get(index) != NOT_EXIST;
+        if (index < 0 || index / mBlockSize >= mLookupTable.size()
+                || mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
+            return false;
+        }
+        return true;
     }
 
     @UsedForTesting
-    public void write(final OutputStream lookupOutStream, final OutputStream contentOutStream)
+    public void write(final OutputStream lookupOutStream, final OutputStream[] contentOutStreams)
             throws IOException {
+         if (contentOutStreams.length != mContentTableCount) {
+             throw new RuntimeException(contentOutStreams.length + " streams are given, but the"
+                     + " table has " + mContentTableCount + " content tables.");
+         }
         for (final int index : mLookupTable) {
-          BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, 4);
+          BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, SIZE_OF_INT_IN_BYTES);
         }
 
-        for (final int index : mContentTable) {
-            BinaryDictEncoderUtils.writeUIntToStream(contentOutStream, index, 4);
+        for (int i = 0; i < contentOutStreams.length; ++i) {
+            for (final int data : mContentTables.get(i)) {
+                BinaryDictEncoderUtils.writeUIntToStream(contentOutStreams[i], data,
+                        SIZE_OF_INT_IN_BYTES);
+            }
         }
     }
 
     @UsedForTesting
-    public void writeToFiles(final File lookupTableFile, final File contentFile)
+    public void writeToFiles(final File lookupTableFile, final File[] contentFiles)
             throws IOException {
-      FileOutputStream lookupTableOutStream = null;
-      FileOutputStream contentOutStream = null;
+        FileOutputStream lookupTableOutStream = null;
+        final FileOutputStream[] contentTableOutStreams = new FileOutputStream[mContentTableCount];
         try {
             lookupTableOutStream = new FileOutputStream(lookupTableFile);
-            contentOutStream = new FileOutputStream(contentFile);
-            write(lookupTableOutStream, contentOutStream);
+            for (int i = 0; i < contentFiles.length; ++i) {
+                contentTableOutStreams[i] = new FileOutputStream(contentFiles[i]);
+            }
+            write(lookupTableOutStream, contentTableOutStreams);
         } finally {
             if (lookupTableOutStream != null) {
                 lookupTableOutStream.close();
             }
-            if (contentOutStream != null) {
-                contentOutStream.close();
+            for (int i = 0; i < contentTableOutStreams.length; ++i) {
+                if (contentTableOutStreams[i] != null) {
+                    contentTableOutStreams[i].close();
+                }
             }
         }
     }
@@ -185,10 +210,14 @@
     }
 
     @UsedForTesting
-    public static SparseTable readFromFiles(final File lookupTableFile, final File contentFile,
+    public static SparseTable readFromFiles(final File lookupTableFile, final File[] contentFiles,
             final int blockSize) throws IOException {
-        final byte[] lookupTable = readFileToByteArray(lookupTableFile);
-        final byte[] content = readFileToByteArray(contentFile);
-        return new SparseTable(lookupTable, content, blockSize);
+        final ArrayList<ArrayList<Integer>> contentTables =
+                new ArrayList<ArrayList<Integer>>(contentFiles.length);
+        for (int i = 0; i < contentFiles.length; ++i) {
+            contentTables.add(convertByteArrayToIntegerArray(readFileToByteArray(contentFiles[i])));
+        }
+        return new SparseTable(convertByteArrayToIntegerArray(readFileToByteArray(lookupTableFile)),
+                contentTables, blockSize);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
index 848277c..acab4f8 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
@@ -37,7 +37,7 @@
  * An implementation of DictDecoder for version 3 binary dictionary.
  */
 @UsedForTesting
-public class Ver3DictDecoder extends DictDecoder {
+public class Ver3DictDecoder extends AbstractDictDecoder {
     private static final String TAG = Ver3DictDecoder.class.getSimpleName();
 
     static {
@@ -47,15 +47,15 @@
     // TODO: implement something sensical instead of just a phony method
     private static native int doNothing();
 
-    protected static class PtNodeReader extends DictDecoder.PtNodeReader {
+    protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader {
         private static int readFrequency(final DictBuffer dictBuffer) {
             return dictBuffer.readUnsignedByte();
         }
     }
 
-    private final File mDictionaryBinaryFile;
+    protected final File mDictionaryBinaryFile;
     private final DictionaryBufferFactory mBufferFactory;
-    private DictBuffer mDictBuffer;
+    protected DictBuffer mDictBuffer;
 
     /* package */ Ver3DictDecoder(final File file, final int factoryFlag) {
         mDictionaryBinaryFile = file;
@@ -169,7 +169,8 @@
             addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams, 
                     addressPointer);
             if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
-                MakedictLog.d("too many bigrams in a PtNode.");
+                throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size()
+                        + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")");
             }
         } else {
             bigrams = null;
@@ -231,4 +232,40 @@
     public boolean hasNextPtNodeArray() {
         return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS;
     }
+
+    @Override
+    public void skipPtNode(final FormatOptions formatOptions) {
+        final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
+        PtNodeReader.readParentAddress(mDictBuffer, formatOptions);
+        BinaryDictIOUtils.skipString(mDictBuffer,
+                (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
+        PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions);
+        if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readFrequency(mDictBuffer);
+        if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) {
+            final int shortcutsSize = mDictBuffer.readUnsignedShort();
+            mDictBuffer.position(mDictBuffer.position() + shortcutsSize
+                    - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
+        }
+        if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) {
+            int bigramCount = 0;
+            while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+                final int bigramFlags = mDictBuffer.readUnsignedByte();
+                switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
+                        mDictBuffer.readUnsignedByte();
+                        break;
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
+                        mDictBuffer.readUnsignedShort();
+                        break;
+                    case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
+                        mDictBuffer.readUnsignedInt24();
+                        break;
+                }
+                if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break;
+            }
+            if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+                throw new RuntimeException("Too many bigrams in a PtNode.");
+            }
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
index 76f0f40..d9e1989 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
@@ -133,12 +133,10 @@
                 countSize);
     }
 
-    private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
-            final FormatOptions formatOptions) {
+    private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) {
         final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
         mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition,
-                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, childrenPos,
-                        formatOptions),
+                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions),
                 FormatSpec.PTNODE_FLAGS_SIZE);
     }
 
@@ -244,7 +242,7 @@
     @Override
     public void writePtNode(final PtNode ptNode, final int parentPosition,
             final FormatOptions formatOptions, final FusionDictionary dict) {
-        writePtNodeFlags(ptNode, parentPosition, formatOptions);
+        writePtNodeFlags(ptNode, formatOptions);
         writeParentPosition(parentPosition, ptNode, formatOptions);
         writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
         writeFrequency(ptNode.mFrequency);
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java
new file mode 100644
index 0000000..07adda6
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java
@@ -0,0 +1,82 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+/**
+ * An implementation of DictUpdater for version 3 binary dictionary.
+ */
+@UsedForTesting
+public class Ver3DictUpdater extends Ver3DictDecoder implements DictUpdater {
+    private OutputStream mOutStream;
+
+    @UsedForTesting
+    public Ver3DictUpdater(final File dictFile, final int factoryType) {
+        // DictUpdater must have an updatable DictBuffer.
+        super(dictFile, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY)
+                ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER);
+        mOutStream = null;
+    }
+
+    private void openStreamAndBuffer() throws FileNotFoundException, IOException {
+        super.openDictBuffer();
+        mOutStream = new FileOutputStream(mDictionaryBinaryFile, true /* append */);
+    }
+
+    private void close() throws IOException {
+        if (mOutStream != null) {
+            mOutStream.close();
+            mOutStream = null;
+        }
+    }
+
+    @Override @UsedForTesting
+    public void deleteWord(final String word) throws IOException, UnsupportedFormatException {
+        if (mOutStream == null) openStreamAndBuffer();
+        mDictBuffer.position(0);
+        readHeader();
+        final int wordPos = getTerminalPosition(word);
+        if (wordPos != FormatSpec.NOT_VALID_WORD) {
+            mDictBuffer.position(wordPos);
+            final int flags = mDictBuffer.readUnsignedByte();
+            mDictBuffer.position(wordPos);
+            mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags));
+        }
+        close();
+    }
+
+    @Override @UsedForTesting
+    public void insertWord(final String word, final int frequency,
+            final ArrayList<WeightedString> bigramStrings,
+            final ArrayList<WeightedString> shortcuts,
+            final boolean isNotAWord, final boolean isBlackListEntry)
+                    throws IOException, UnsupportedFormatException {
+        if (mOutStream == null) openStreamAndBuffer();
+        DynamicBinaryDictIOUtils.insertWord(this, mOutStream, word, frequency, bigramStrings,
+                shortcuts, isNotAWord, isBlackListEntry);
+        close();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 0aa4319..5372907 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -23,6 +23,7 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import android.util.Log;
 
@@ -36,21 +37,24 @@
  * An implementation of binary dictionary decoder for version 4 binary dictionary.
  */
 @UsedForTesting
-public class Ver4DictDecoder extends DictDecoder {
+public class Ver4DictDecoder extends AbstractDictDecoder {
     private static final String TAG = Ver4DictDecoder.class.getSimpleName();
 
     private static final int FILETYPE_TRIE = 1;
     private static final int FILETYPE_FREQUENCY = 2;
     private static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3;
-    private static final int FILETYPE_BIGRAM = 4;
+    private static final int FILETYPE_BIGRAM_FREQ = 4;
+    private static final int FILETYPE_SHORTCUT = 5;
 
     private final File mDictDirectory;
     private final DictionaryBufferFactory mBufferFactory;
-    private DictBuffer mDictBuffer;
+    protected DictBuffer mDictBuffer;
     private DictBuffer mFrequencyBuffer;
     private DictBuffer mTerminalAddressTableBuffer;
     private DictBuffer mBigramBuffer;
+    private DictBuffer mShortcutBuffer;
     private SparseTable mBigramAddressTable;
+    private SparseTable mShortcutAddressTable;
 
     @UsedForTesting
     /* package */ Ver4DictDecoder(final File dictDirectory, final int factoryFlag) {
@@ -85,9 +89,14 @@
         } else if (fileType == FILETYPE_TERMINAL_ADDRESS_TABLE) {
             return new File(mDictDirectory,
                     mDictDirectory.getName() + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
-        } else if (fileType == FILETYPE_BIGRAM) {
+        } else if (fileType == FILETYPE_BIGRAM_FREQ) {
             return new File(mDictDirectory,
-                    mDictDirectory.getName() + FormatSpec.BIGRAM_FILE_EXTENSION);
+                    mDictDirectory.getName() + FormatSpec.BIGRAM_FILE_EXTENSION
+                            + FormatSpec.BIGRAM_FREQ_CONTENT_ID);
+        } else if (fileType == FILETYPE_SHORTCUT) {
+            return new File(mDictDirectory,
+                    mDictDirectory.getName() + FormatSpec.SHORTCUT_FILE_EXTENSION
+                            + FormatSpec.SHORTCUT_CONTENT_ID);
         } else {
             throw new RuntimeException("Unsupported kind of file : " + fileType);
         }
@@ -95,13 +104,14 @@
 
     @Override
     public void openDictBuffer() throws FileNotFoundException, IOException {
-        final String filename = mDictDirectory.getName();
         mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE));
         mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY));
         mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer(
                 getFile(FILETYPE_TERMINAL_ADDRESS_TABLE));
-        mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM));
+        mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM_FREQ));
         loadBigramAddressSparseTable();
+        mShortcutBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_SHORTCUT));
+        loadShortcutAddressSparseTable();
     }
 
     @Override
@@ -127,15 +137,27 @@
     }
 
     private void loadBigramAddressSparseTable() throws IOException {
-        final File lookupIndexFile = new File(mDictDirectory,
-                mDictDirectory.getName() + FormatSpec.BIGRAM_LOOKUP_TABLE_FILE_EXTENSION);
-        final File contentFile = new File(mDictDirectory,
-                mDictDirectory.getName() + FormatSpec.BIGRAM_ADDRESS_TABLE_FILE_EXTENSION);
-        mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, contentFile,
+        final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName()
+                + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
+        final File freqsFile = new File(mDictDirectory, mDictDirectory.getName()
+                + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
+                + FormatSpec.BIGRAM_FREQ_CONTENT_ID);
+        mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, new File[] { freqsFile },
                 FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE);
     }
 
-    protected static class PtNodeReader extends DictDecoder.PtNodeReader {
+    // TODO: Let's have something like SparseTableContentsReader in this class.
+    private void loadShortcutAddressSparseTable() throws IOException {
+        final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName()
+                + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
+        final File contentFile = new File(mDictDirectory, mDictDirectory.getName()
+                + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX
+                + FormatSpec.SHORTCUT_CONTENT_ID);
+        mShortcutAddressTable = SparseTable.readFromFiles(lookupIndexFile,
+                new File[] { contentFile }, FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE);
+    }
+
+    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);
             return frequencyBuffer.readUnsignedByte();
@@ -146,6 +168,23 @@
         }
     }
 
+    private ArrayList<WeightedString> readShortcuts(final int terminalId) {
+        if (mShortcutAddressTable.get(0, terminalId) == SparseTable.NOT_EXIST) return null;
+
+        final ArrayList<WeightedString> ret = CollectionUtils.newArrayList();
+        final int posOfShortcuts = mShortcutAddressTable.get(FormatSpec.SHORTCUT_CONTENT_INDEX,
+                terminalId);
+        mShortcutBuffer.position(posOfShortcuts);
+        while (true) {
+            final int flags = mShortcutBuffer.readUnsignedByte();
+            final String word = CharEncoding.readString(mShortcutBuffer);
+            ret.add(new WeightedString(word,
+                    flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY));
+            if (0 == (flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
+        }
+        return ret;
+    }
+
     // TODO: Make this buffer thread safe.
     // TODO: Support words longer than FormatSpec.MAX_WORD_LENGTH.
     private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
@@ -196,19 +235,12 @@
             childrenAddress += addressPointer;
         }
         addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options);
-        final ArrayList<WeightedString> shortcutTargets;
-        if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
-            // readShortcut will add shortcuts to shortcutTargets.
-            shortcutTargets = new ArrayList<WeightedString>();
-            addressPointer += PtNodeReader.readShortcut(mDictBuffer, shortcutTargets);
-        } else {
-            shortcutTargets = null;
-        }
+        final ArrayList<WeightedString> shortcutTargets = readShortcuts(terminalId);
 
         final ArrayList<PendingAttribute> bigrams;
         if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
             bigrams = new ArrayList<PendingAttribute>();
-            final int posOfBigrams = mBigramAddressTable.get(terminalId);
+            final int posOfBigrams = mBigramAddressTable.get(0 /* contentTableIndex */, terminalId);
             mBigramBuffer.position(posOfBigrams);
             while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
                 // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE,
@@ -224,7 +256,8 @@
                 if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break;
             }
             if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
-                MakedictLog.d("too many bigrams in a node.");
+                throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size()
+                        + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")");
             }
         } else {
             bigrams = null;
@@ -293,4 +326,14 @@
     public boolean hasNextPtNodeArray() {
         return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS;
     }
+
+    @Override
+    public void skipPtNode(final FormatOptions formatOptions) {
+        final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
+        PtNodeReader.readParentAddress(mDictBuffer, formatOptions);
+        BinaryDictIOUtils.skipString(mDictBuffer,
+                (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
+        if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readTerminalId(mDictBuffer);
+        PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index 4c25faf..f9dcacf 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -26,7 +26,6 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
@@ -44,19 +43,149 @@
     private byte[] mTrieBuf;
     private int mTriePos;
     private int mHeaderSize;
-    private SparseTable mBigramAddressTable;
     private OutputStream mTrieOutStream;
     private OutputStream mFreqOutStream;
     private OutputStream mTerminalAddressTableOutStream;
-    private OutputStream mBigramOutStream;
     private File mDictDir;
     private String mBaseFilename;
+    private BigramContentWriter mBigramWriter;
+    private ShortcutContentWriter mShortcutWriter;
 
     @UsedForTesting
     public Ver4DictEncoder(final File dictPlacedDir) {
         mDictPlacedDir = dictPlacedDir;
     }
 
+    private interface SparseTableContentWriterInterface {
+        public void write(final OutputStream outStream) throws IOException;
+    }
+
+    private static class SparseTableContentWriter {
+        private final int mContentCount;
+        private final SparseTable mSparseTable;
+        private final File mLookupTableFile;
+        protected final File mBaseDir;
+        private final File[] mAddressTableFiles;
+        private final File[] mContentFiles;
+        protected final OutputStream[] mContentOutStreams;
+
+        public SparseTableContentWriter(final String name, final int contentCount,
+                final int initialCapacity, final int blockSize, final File baseDir,
+                final String[] contentFilenames, final String[] contentIds) {
+            if (contentFilenames.length != contentIds.length) {
+                throw new RuntimeException("The length of contentFilenames and the length of"
+                        + " contentIds are different " + contentFilenames.length + ", "
+                        + contentIds.length);
+            }
+            mContentCount = contentCount;
+            mSparseTable = new SparseTable(initialCapacity, blockSize, contentCount);
+            mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX);
+            mAddressTableFiles = new File[mContentCount];
+            mContentFiles = new File[mContentCount];
+            mBaseDir = baseDir;
+            for (int i = 0; i < mContentCount; ++i) {
+                mAddressTableFiles[i] = new File(mBaseDir,
+                        name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]);
+                mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]);
+            }
+            mContentOutStreams = new OutputStream[mContentCount];
+        }
+
+        public void openStreams() throws FileNotFoundException {
+            for (int i = 0; i < mContentCount; ++i) {
+                mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]);
+            }
+        }
+
+        protected void write(final int contentIndex, final int index,
+                final SparseTableContentWriterInterface writer) throws IOException {
+            mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length());
+            writer.write(mContentOutStreams[contentIndex]);
+            mContentOutStreams[contentIndex].flush();
+        }
+
+        public void closeStreams() throws IOException {
+            mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles);
+            for (int i = 0; i < mContentCount; ++i) {
+                mContentOutStreams[i].close();
+            }
+        }
+    }
+
+    private static class BigramContentWriter extends SparseTableContentWriter {
+
+        public BigramContentWriter(final String name, final int initialCapacity,
+                final File baseDir) {
+            super(name + FormatSpec.BIGRAM_FILE_EXTENSION, FormatSpec.BIGRAM_CONTENT_COUNT,
+                    initialCapacity, FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+                    new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION },
+                    new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID });
+        }
+
+        public void writeBigramsForOneWord(final int terminalId,
+                final Iterator<WeightedString> bigramIterator, final FusionDictionary dict)
+                        throws IOException {
+            write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId,
+                    new SparseTableContentWriterInterface() {
+                        @Override
+                        public void write(final OutputStream outStream) throws IOException {
+                            writeBigramsForOneWordInternal(outStream, bigramIterator, dict);
+                        }
+            });
+        }
+
+        private void writeBigramsForOneWordInternal(final OutputStream outStream,
+                final Iterator<WeightedString> bigramIterator, final FusionDictionary dict)
+                        throws IOException {
+            while (bigramIterator.hasNext()) {
+                final WeightedString bigram = bigramIterator.next();
+                final PtNode target =
+                        FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord);
+                final int unigramFrequencyForThisWord = target.mFrequency;
+                final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(
+                        bigramIterator.hasNext(), 0, bigram.mFrequency,
+                        unigramFrequencyForThisWord, bigram.mWord);
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, bigramFlags,
+                        FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, target.mTerminalId,
+                        FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE);
+            }
+        }
+    }
+
+    private static class ShortcutContentWriter extends SparseTableContentWriter {
+        public ShortcutContentWriter(final String name, final int initialCapacity,
+                final File baseDir) {
+            super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, FormatSpec.SHORTCUT_CONTENT_COUNT,
+                    initialCapacity, FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir,
+                    new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION },
+                    new String[] { FormatSpec.SHORTCUT_CONTENT_ID });
+        }
+
+        public void writeShortcutForOneWord(final int terminalId,
+                final Iterator<WeightedString> shortcutIterator) throws IOException {
+            write(FormatSpec.SHORTCUT_CONTENT_INDEX, terminalId,
+                    new SparseTableContentWriterInterface() {
+                        @Override
+                        public void write(final OutputStream outStream) throws IOException {
+                            writeShortcutForOneWordInternal(outStream, shortcutIterator);
+                        }
+                    });
+        }
+
+        private void writeShortcutForOneWordInternal(final OutputStream outStream,
+                final Iterator<WeightedString> shortcutIterator) throws IOException {
+            while (shortcutIterator.hasNext()) {
+                final WeightedString target = shortcutIterator.next();
+                final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags(
+                        shortcutIterator.hasNext(), target.mFrequency);
+                BinaryDictEncoderUtils.writeUIntToStream(outStream, shortcutFlags,
+                        FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
+                CharEncoding.writeString(outStream, target.mWord);
+            }
+        }
+    }
+
     private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions)
             throws FileNotFoundException, IOException {
         final FileHeader header = new FileHeader(0, dictOptions, formatOptions);
@@ -66,8 +195,6 @@
         final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION);
         final File terminalAddressTableFile = new File(mDictDir,
                 mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
-        final File bigramFile = new File(mDictDir,
-                mBaseFilename + FormatSpec.BIGRAM_FILE_EXTENSION);
         if (!mDictDir.isDirectory()) {
             if (mDictDir.exists()) mDictDir.delete();
             mDictDir.mkdirs();
@@ -78,7 +205,6 @@
         mTrieOutStream = new FileOutputStream(trieFile);
         mFreqOutStream = new FileOutputStream(freqFile);
         mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile);
-        mBigramOutStream = new FileOutputStream(bigramFile);
     }
 
     private void close() throws IOException {
@@ -92,14 +218,10 @@
             if (mTerminalAddressTableOutStream != null) {
                 mTerminalAddressTableOutStream.close();
             }
-            if (mBigramOutStream != null) {
-                mBigramOutStream.close();
-            }
         } finally {
             mTrieOutStream = null;
             mFreqOutStream = null;
             mTerminalAddressTableOutStream = null;
-            mBigramOutStream = null;
         }
     }
 
@@ -135,10 +257,10 @@
         if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes);
 
         writeTerminalData(flatNodes, terminalCount);
-        mBigramAddressTable = new SparseTable(terminalCount,
-                FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE);
+        mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir);
         writeBigrams(flatNodes, dict);
-        writeBigramAddressSparseTable();
+        mShortcutWriter = new ShortcutContentWriter(mBaseFilename, terminalCount, mDictDir);
+        writeShortcuts(flatNodes);
 
         final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1);
         final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize;
@@ -181,12 +303,10 @@
                 countSize);
     }
 
-    private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
-            final FormatOptions formatOptions) {
+    private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) {
         final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
         mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
-                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mTriePos, childrenPos,
-                        formatOptions),
+                BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions),
                 FormatSpec.PTNODE_FLAGS_SIZE);
     }
 
@@ -222,65 +342,31 @@
         }
     }
 
-    private void writeShortcuts(ArrayList<WeightedString> shortcuts) {
-        if (null == shortcuts || shortcuts.isEmpty()) return;
-
-        final int indexOfShortcutByteSize = mTriePos;
-        mTriePos += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE;
-        final Iterator<WeightedString> shortcutIterator = shortcuts.iterator();
-        while (shortcutIterator.hasNext()) {
-            final WeightedString target = shortcutIterator.next();
-            final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags(
-                    shortcutIterator.hasNext(),
-                    target.mFrequency);
-            mTrieBuf[mTriePos++] = (byte)shortcutFlags;
-            final int shortcutShift = CharEncoding.writeString(mTrieBuf, mTriePos,
-                    target.mWord);
-            mTriePos += shortcutShift;
-        }
-        final int shortcutByteSize = mTriePos - indexOfShortcutByteSize;
-        if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) {
-            throw new RuntimeException("Shortcut list too large : " + shortcutByteSize);
-        }
-        BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, indexOfShortcutByteSize,
-                shortcutByteSize, FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
-    }
-
     private void writeBigrams(final ArrayList<PtNodeArray> flatNodes, final FusionDictionary dict)
             throws IOException {
-        final ByteArrayOutputStream bigramBuffer = new ByteArrayOutputStream();
-
+        mBigramWriter.openStreams();
         for (final PtNodeArray nodeArray : flatNodes) {
             for (final PtNode ptNode : nodeArray.mData) {
                 if (ptNode.mBigrams != null) {
-                    final int startPos = bigramBuffer.size();
-                    mBigramAddressTable.set(ptNode.mTerminalId, startPos);
-                    final Iterator<WeightedString> bigramIterator = ptNode.mBigrams.iterator();
-                    while (bigramIterator.hasNext()) {
-                        final WeightedString bigram = bigramIterator.next();
-                        final PtNode target =
-                            FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord);
-                        final int unigramFrequencyForThisWord = target.mFrequency;
-                        final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(
-                                bigramIterator.hasNext(), 0, bigram.mFrequency,
-                                unigramFrequencyForThisWord, bigram.mWord);
-                        BinaryDictEncoderUtils.writeUIntToStream(bigramBuffer, bigramFlags,
-                                FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
-                        BinaryDictEncoderUtils.writeUIntToStream(bigramBuffer, target.mTerminalId,
-                                FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE);
-                    }
+                    mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId,
+                            ptNode.mBigrams.iterator(), dict);
                 }
             }
         }
-        bigramBuffer.writeTo(mBigramOutStream);
+        mBigramWriter.closeStreams();
     }
 
-    private void writeBigramAddressSparseTable() throws IOException {
-        final File lookupIndexFile =
-                new File(mDictDir, mBaseFilename + FormatSpec.BIGRAM_LOOKUP_TABLE_FILE_EXTENSION);
-        final File contentFile =
-                new File(mDictDir, mBaseFilename + FormatSpec.BIGRAM_ADDRESS_TABLE_FILE_EXTENSION);
-        mBigramAddressTable.writeToFiles(lookupIndexFile, contentFile);
+    private void writeShortcuts(final ArrayList<PtNodeArray> flatNodes) throws IOException {
+        mShortcutWriter.openStreams();
+        for (final PtNodeArray nodeArray : flatNodes) {
+            for (final PtNode ptNode : nodeArray.mData) {
+                if (ptNode.mShortcutTargets != null && !ptNode.mShortcutTargets.isEmpty()) {
+                    mShortcutWriter.writeShortcutForOneWord(ptNode.mTerminalId,
+                            ptNode.mShortcutTargets.iterator());
+                }
+            }
+        }
+        mShortcutWriter.closeStreams();
     }
 
     @Override
@@ -292,14 +378,13 @@
     @Override
     public void writePtNode(final PtNode ptNode, final int parentPosition,
             final FormatOptions formatOptions, final FusionDictionary dict) {
-        writePtNodeFlags(ptNode, parentPosition, formatOptions);
+        writePtNodeFlags(ptNode, formatOptions);
         writeParentPosition(parentPosition, ptNode, formatOptions);
         writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
         if (ptNode.isTerminal()) {
             writeTerminalId(ptNode.mTerminalId);
         }
         writeChildrenPosition(ptNode, formatOptions);
-        writeShortcuts(ptNode.mShortcutTargets);
     }
 
     private void writeTerminalData(final ArrayList<PtNodeArray> flatNodes,
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java
new file mode 100644
index 0000000..3d8f186
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java
@@ -0,0 +1,59 @@
+/*
+ * 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.makedict;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * An implementation of DictUpdater for version 4 binary dictionary.
+ */
+@UsedForTesting
+public class Ver4DictUpdater extends Ver4DictDecoder implements DictUpdater {
+
+    @UsedForTesting
+    public Ver4DictUpdater(final File dictDirectory, final int factoryType) {
+        // DictUpdater must have an updatable DictBuffer.
+        super(dictDirectory, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY)
+                ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER);
+    }
+
+    @Override
+    public void deleteWord(final String word) throws IOException, UnsupportedFormatException {
+        if (mDictBuffer == null) openDictBuffer();
+        readHeader();
+        final int wordPos = getTerminalPosition(word);
+        if (wordPos != FormatSpec.NOT_VALID_WORD) {
+            mDictBuffer.position(wordPos);
+            final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer);
+            mDictBuffer.position(wordPos);
+            mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags));
+        }
+    }
+
+    @Override
+    public void insertWord(final String word, final int frequency,
+        final ArrayList<WeightedString> bigramStrings, final ArrayList<WeightedString> shortcuts,
+        final boolean isNotAWord, final boolean isBlackListEntry)
+                throws IOException, UnsupportedFormatException {
+        // TODO: Implement this method.
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
index 7cf4f0c..a1e3600 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -138,7 +138,7 @@
         final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ?
                 (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) :
                         FREQUENCY_FOR_TYPED;
-        addWordDynamically(word1, null /* the "shortcut" parameter is null */, frequency,
+        addWordDynamically(word1, null /* shortcutTarget */, frequency, 0 /* shortcutFreq */,
                 false /* isNotAWord */);
         // Do not insert a word as a bigram of itself
         if (word1.equals(word0)) {
@@ -171,11 +171,11 @@
         final OnAddWordListener listener = new OnAddWordListener() {
             @Override
             public void setUnigram(final String word, final String shortcutTarget,
-                    final int frequency) {
+                    final int frequency, final int shortcutFreq) {
                 if (DBG_SAVE_RESTORE) {
                     Log.d(TAG, "load unigram: " + word + "," + frequency);
                 }
-                addWord(word, shortcutTarget, frequency, false /* isNotAWord */);
+                addWord(word, shortcutTarget, frequency, shortcutFreq, false /* isNotAWord */);
                 ++profTotalCount[0];
             }
 
@@ -230,10 +230,15 @@
         mSessions.remove(session);
     }
 
+    @UsedForTesting
     public void clearAndFlushDictionary() {
         // Clear the node structure on memory
         clear();
         // Then flush the cleared state of the dictionary on disk.
         asyncFlashAllBinaryDictionary();
     }
+
+    /* package */ void decayIfNeeded() {
+        runGCIfRequired(false /* mindsBlockByGC */);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java
new file mode 100644
index 0000000..e9ca662
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java
@@ -0,0 +1,66 @@
+/*
+ * 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.personalization;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Broadcast receiver for periodically updating decaying dictionaries.
+ */
+public class DictionaryDecayBroadcastReciever extends BroadcastReceiver {
+    /**
+     * The root domain for the personalization.
+     */
+    private static final String PERSONALIZATION_DOMAIN =
+            "com.android.inputmethod.latin.personalization";
+
+    /**
+     * The action of the intent to tell the time to decay dictionaries.
+     */
+    private static final String DICTIONARY_DECAY_INTENT_ACTION =
+            PERSONALIZATION_DOMAIN + ".DICT_DECAY";
+
+    /**
+     * Interval to update for decaying dictionaries.
+     */
+    private static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60);
+
+    public static void setUpIntervalAlarmForDictionaryDecaying(Context context) {
+        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
+        final Intent updateIntent = new Intent(DICTIONARY_DECAY_INTENT_ACTION);
+        updateIntent.setClass(context, DictionaryDecayBroadcastReciever.class);
+        final long alarmTime =  System.currentTimeMillis() + DICTIONARY_DECAY_INTERVAL;
+        final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 /* requestCode */,
+                updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+        if (null != alarmManager) alarmManager.setInexactRepeating(AlarmManager.RTC,
+                alarmTime, DICTIONARY_DECAY_INTERVAL, pendingIntent);
+    }
+
+    @Override
+    public void onReceive(final Context context, final Intent intent) {
+        final String action = intent.getAction();
+        if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) {
+            PersonalizationHelper.tryDecayingAllOpeningUserHistoryDictionary();
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
index 039b253..6f152bb 100644
--- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
@@ -75,15 +75,21 @@
     /**
      * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
      * are done to update the binary dictionary.
+     * @param word The word to add.
+     * @param shortcutTarget A shortcut target for this word, or null if none.
+     * @param frequency The frequency for this unigram.
+     * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored
+     *   if shortcutTarget is null.
+     * @param isNotAWord true if this is not a word, i.e. shortcut only.
      */
     @Override
     public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
-            final boolean isNotAWord) {
+            final int shortcutFreq, final boolean isNotAWord) {
         if (mBigramList.size() > mMaxHistoryBigrams * 2) {
             // Too many entries: just stop adding new vocabulary and wait next refresh.
             return;
         }
-        mExpandableDictionary.addWord(word, shortcutTarget, frequency);
+        mExpandableDictionary.addWord(word, shortcutTarget, frequency, shortcutFreq);
         mBigramList.addBigram(null, word, (byte)frequency);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
index 8c9484b..221ddee 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -29,7 +29,6 @@
 public class PersonalizationHelper {
     private static final String TAG = PersonalizationHelper.class.getSimpleName();
     private static final boolean DEBUG = false;
-
     private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
             sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap();
 
@@ -62,6 +61,18 @@
         }
     }
 
+    public static void tryDecayingAllOpeningUserHistoryDictionary() {
+        for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry
+                : sLangUserHistoryDictCache.entrySet()) {
+            if (entry.getValue() != null) {
+                final UserHistoryDictionary dict = entry.getValue().get();
+                if (dict != null) {
+                    dict.decayIfNeeded();
+                }
+            }
+        }
+    }
+
     public static void registerPersonalizationDictionaryUpdateSession(final Context context,
             final PersonalizationDictionaryUpdateSession session, String locale) {
         final PersonalizationPredictionDictionary predictionDictionary =
diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java
index b499c26..a23e377 100644
--- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java
@@ -21,6 +21,7 @@
 import android.preference.PreferenceActivity;
 
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.FragmentUtils;
 
 public final class DebugSettingsActivity extends PreferenceActivity {
     private static final String DEFAULT_FRAGMENT = DebugSettings.class.getName();
@@ -38,4 +39,10 @@
         super.onCreate(savedInstanceState);
         setTitle(R.string.english_ime_debug_settings);
     }
+
+    // TODO: Uncomment the override annotation once we start using SDK version 19.
+    // @Override
+    public boolean isValidFragment(String fragmentName) {
+        return FragmentUtils.isValidFragment(fragmentName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
index 6c38186..c899507 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.settings;
 
+import com.android.inputmethod.latin.utils.FragmentUtils;
+
 import android.content.Intent;
 import android.preference.PreferenceActivity;
 
@@ -32,4 +34,10 @@
         intent.putExtra(EXTRA_NO_HEADERS, true);
         return intent;
     }
+
+    // TODO: Uncomment the override annotation once we start using SDK version 19.
+    // @Override
+    public boolean isValidFragment(String fragmentName) {
+        return FragmentUtils.isValidFragment(fragmentName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index ee322e9..f331c78 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -24,6 +24,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.R;
@@ -45,8 +46,9 @@
  */
 public final class SettingsValues {
     private static final String TAG = SettingsValues.class.getSimpleName();
-    // "floatNegativeInfinity" is a special marker string for Float.NEGATIVE_INFINITE
-    // currently used for auto-correction
+    // "floatMaxValue" and "floatNegativeInfinity" are special marker strings for
+    // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings.
+    private static final String FLOAT_MAX_VALUE_MARKER_STRING = "floatMaxValue";
     private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity";
 
     // From resources:
@@ -56,6 +58,7 @@
     public final int[] mWordConnectors;
     public final SuggestedWords mSuggestPuncList;
     public final String mWordSeparators;
+    public final int mSentenceSeparator;
     public final CharSequence mHintToSaveText;
     public final boolean mCurrentLanguageHasSpaces;
 
@@ -119,6 +122,7 @@
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mWordSeparators = res.getString(R.string.symbols_word_separators);
+        mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
         mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
         mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
 
@@ -186,6 +190,7 @@
         Arrays.sort(mSymbolsFollowedBySpace);
         mWordConnectors = new int[] { '\'', '-' };
         Arrays.sort(mWordConnectors);
+        mSentenceSeparator = Constants.CODE_PERIOD;
         final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" };
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\"";
@@ -343,24 +348,28 @@
         final String[] autoCorrectionThresholdValues = res.getStringArray(
                 R.array.auto_correction_threshold_values);
         // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
-        float autoCorrectionThreshold = Float.MAX_VALUE;
+        final float autoCorrectionThreshold;
         try {
             final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting);
             if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) {
                 final String val = autoCorrectionThresholdValues[arrayIndex];
-                if (FLOAT_NEGATIVE_INFINITY_MARKER_STRING.equals(val)) {
+                if (FLOAT_MAX_VALUE_MARKER_STRING.equals(val)) {
+                    autoCorrectionThreshold = Float.MAX_VALUE;
+                } else if (FLOAT_NEGATIVE_INFINITY_MARKER_STRING.equals(val)) {
                     autoCorrectionThreshold = Float.NEGATIVE_INFINITY;
                 } else {
                     autoCorrectionThreshold = Float.parseFloat(val);
                 }
+            } else {
+                autoCorrectionThreshold = Float.MAX_VALUE;
             }
-        } catch (NumberFormatException e) {
+        } catch (final NumberFormatException e) {
             // Whenever the threshold settings are correct, never come here.
-            autoCorrectionThreshold = Float.MAX_VALUE;
             Log.w(TAG, "Cannot load auto correction threshold setting."
                     + " currentAutoCorrectionSetting: " + currentAutoCorrectionSetting
                     + ", autoCorrectionThresholdValues: "
                     + Arrays.toString(autoCorrectionThresholdValues), e);
+            return Float.MAX_VALUE;
         }
         return autoCorrectionThreshold;
     }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index eb6d7c1..503b18b 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -204,10 +204,20 @@
         return AndroidSpellCheckerSessionFactory.newInstance(this);
     }
 
-    public static SuggestionsInfo getNotInDictEmptySuggestions() {
-        return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
+    /**
+     * Returns an empty SuggestionsInfo with flags signaling the word is not in the dictionary.
+     * @param reportAsTypo whether this should include the flag LOOKS_LIKE_TYPO, for red underline.
+     * @return the empty SuggestionsInfo with the appropriate flags set.
+     */
+    public static SuggestionsInfo getNotInDictEmptySuggestions(final boolean reportAsTypo) {
+        return new SuggestionsInfo(reportAsTypo ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0,
+                EMPTY_STRING_ARRAY);
     }
 
+    /**
+     * Returns an empty suggestionInfo with flags signaling the word is in the dictionary.
+     * @return the empty SuggestionsInfo with the appropriate flags set.
+     */
     public static SuggestionsInfo getInDictEmptySuggestions() {
         return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
                 EMPTY_STRING_ARRAY);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 69f9a46..d6e5b75 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -161,6 +161,12 @@
         }
     }
 
+    private static final int CHECKABILITY_CHECKABLE = 0;
+    private static final int CHECKABILITY_TOO_MANY_NON_LETTERS = 1;
+    private static final int CHECKABILITY_CONTAINS_PERIOD = 2;
+    private static final int CHECKABILITY_EMAIL_OR_URL = 3;
+    private static final int CHECKABILITY_FIRST_LETTER_UNCHECKABLE = 4;
+    private static final int CHECKABILITY_TOO_SHORT = 5;
     /**
      * Finds out whether a particular string should be filtered out of spell checking.
      *
@@ -171,10 +177,10 @@
      *
      * @param text the string to evaluate.
      * @param script the identifier for the script this spell checker recognizes
-     * @return true if we should filter this text out, false otherwise
+     * @return one of the FILTER_OUT_* constants above.
      */
-    private static boolean shouldFilterOut(final String text, final int script) {
-        if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
+    private static int getCheckabilityInScript(final String text, final int script) {
+        if (TextUtils.isEmpty(text) || text.length() <= 1) return CHECKABILITY_TOO_SHORT;
 
         // TODO: check if an equivalent processing can't be done more quickly with a
         // compiled regexp.
@@ -182,7 +188,7 @@
         final int firstCodePoint = text.codePointAt(0);
         // Filter out words that don't start with a letter or an apostrophe
         if (!isLetterCheckableByLanguage(firstCodePoint, script)
-                && '\'' != firstCodePoint) return true;
+                && '\'' != firstCodePoint) return CHECKABILITY_FIRST_LETTER_UNCHECKABLE;
 
         // Filter contents
         final int length = text.length();
@@ -193,13 +199,21 @@
             // Any word containing a SLASH is probably either an ad-hoc combination of two
             // words or a URI - in either case we don't want to spell check that
             if (Constants.CODE_COMMERCIAL_AT == codePoint || Constants.CODE_SLASH == codePoint) {
-                return true;
+                return CHECKABILITY_EMAIL_OR_URL;
+            }
+            // If the string contains a period, native returns strange suggestions (it seems
+            // to return suggestions for everything up to the period only and to ignore the
+            // rest), so we suppress lookup if there is a period.
+            // TODO: investigate why native returns these suggestions and remove this code.
+            if (Constants.CODE_PERIOD == codePoint) {
+                return CHECKABILITY_CONTAINS_PERIOD;
             }
             if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
         }
         // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
         // in this word are letters
-        return (letterCount * 4 < length * 3);
+        return (letterCount * 4 < length * 3)
+                ? CHECKABILITY_TOO_MANY_NON_LETTERS : CHECKABILITY_CHECKABLE;
     }
 
     /**
@@ -256,16 +270,20 @@
                         cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
             }
 
-            if (shouldFilterOut(inText, mScript)) {
+            final int checkability = getCheckabilityInScript(inText, mScript);
+            if (CHECKABILITY_CHECKABLE != checkability) {
                 DictAndKeyboard dictInfo = null;
                 try {
                     dictInfo = mDictionaryPool.pollWithDefaultTimeout();
                     if (!DictionaryPool.isAValidDictionary(dictInfo)) {
-                        return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                        return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
+                                false /* reportAsTypo */);
                     }
                     return dictInfo.mDictionary.isValidWord(inText)
                             ? AndroidSpellCheckerService.getInDictEmptySuggestions()
-                            : AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                            : AndroidSpellCheckerService.getNotInDictEmptySuggestions(
+                                    CHECKABILITY_CONTAINS_PERIOD == checkability
+                                    /* reportAsTypo */);
                 } finally {
                     if (null != dictInfo) {
                         if (!mDictionaryPool.offer(dictInfo)) {
@@ -290,7 +308,8 @@
             try {
                 dictInfo = mDictionaryPool.pollWithDefaultTimeout();
                 if (!DictionaryPool.isAValidDictionary(dictInfo)) {
-                    return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                    return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
+                            false /* reportAsTypo */);
                 }
                 final WordComposer composer = new WordComposer();
                 final int length = text.length();
@@ -351,7 +370,8 @@
                 throw e;
             } else {
                 Log.e(TAG, "Exception while spellcheking", e);
-                return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+                return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
+                        false /* reportAsTypo */);
             }
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
index 119ca47..df9a761 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.spellcheck;
 
+import com.android.inputmethod.latin.utils.FragmentUtils;
+
 import android.content.Intent;
 import android.os.Bundle;
 import android.preference.PreferenceActivity;
@@ -24,6 +26,8 @@
  * Spell checker preference screen.
  */
 public final class SpellCheckerSettingsActivity extends PreferenceActivity {
+    private static final String DEFAULT_FRAGMENT = SpellCheckerSettingsFragment.class.getName();
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -32,8 +36,14 @@
     @Override
     public Intent getIntent() {
         final Intent modIntent = new Intent(super.getIntent());
-        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, SpellCheckerSettingsFragment.class.getName());
+        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
         modIntent.putExtra(EXTRA_NO_HEADERS, true);
         return modIntent;
     }
+
+    // TODO: Uncomment the override annotation once we start using SDK version 19.
+    // @Override
+    public boolean isValidFragment(String fragmentName) {
+        return FragmentUtils.isValidFragment(fragmentName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
index 44b2016..d87f6f3 100644
--- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -25,6 +25,7 @@
 import android.text.TextUtils;
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
 
@@ -61,10 +62,8 @@
                 StringUtils.appendToCommaSplittableTextIfNotExists(
                         IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
         final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName);
-        return new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark,
-                localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue
-                        + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
-                        + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, false, false);
+        return buildInputMethodSubtype(
+                nameId, localeString, layoutExtraValue, additionalSubtypeExtraValue);
     }
 
     public static String getPrefSubtype(final InputMethodSubtype subtype) {
@@ -137,4 +136,32 @@
         }
         return sb.toString();
     }
+
+    private static InputMethodSubtype buildInputMethodSubtype(int nameId, String localeString,
+            String layoutExtraValue, String additionalSubtypeExtraValue) {
+        // CAVEAT! If you want to change subtypeId after changing the extra values,
+        // you must change "getInputMethodSubtypeId". But it will remove the additional keyboard
+        // from the current users. So, you should be really careful to change it.
+        final int subtypeId = getInputMethodSubtypeId(nameId, localeString, layoutExtraValue,
+                additionalSubtypeExtraValue);
+        final String extraValue;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue
+                    + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
+                    + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+        } else {
+            extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue;
+        }
+        return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId,
+                R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, extraValue,
+                false, false, subtypeId);
+    }
+
+    private static int getInputMethodSubtypeId(int nameId, String localeString,
+            String layoutExtraValue, String additionalSubtypeExtraValue) {
+        // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19.
+        return (new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark,
+                localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue,
+                        false, false)).hashCode();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 60b24d5..3d4404a 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -21,6 +21,7 @@
 
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.settings.SettingsValues;
 
 import java.util.Locale;
 
@@ -60,11 +61,6 @@
                 || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode;
     }
 
-    private static boolean isPeriod(final int codePoint) {
-        // TODO: make this a resource.
-        return codePoint == Constants.CODE_PERIOD || codePoint == Constants.CODE_ARMENIAN_PERIOD;
-    }
-
     /**
      * Determine what caps mode should be in effect at the current offset in
      * the text. Only the mode bits set in <var>reqModes</var> will be
@@ -78,7 +74,7 @@
      * @param reqModes The modes to be checked: may be any combination of
      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
      * {@link TextUtils#CAP_MODE_SENTENCES}.
-     * @param locale The locale to consider for capitalization rules
+     * @param settingsValues The current settings values.
      * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
      *
      * @return Returns the actual capitalization modes that can be in effect
@@ -86,8 +82,8 @@
      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
      * {@link TextUtils#CAP_MODE_SENTENCES}.
      */
-    public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale,
-            final boolean hasSpaceBefore) {
+    public static int getCapsMode(final CharSequence cs, final int reqModes,
+            final SettingsValues settingsValues, final boolean hasSpaceBefore) {
         // Quick description of what we want to do:
         // CAP_MODE_CHARACTERS is always on.
         // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
@@ -172,7 +168,7 @@
         // mark as the exact thing quoted and handling the surrounding punctuation independently,
         // e.g. <<Did he say, "let's go home"?>>
         // Hence, specifically for English, we treat this special case here.
-        if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
+        if (Locale.ENGLISH.getLanguage().equals(settingsValues.mLocale.getLanguage())) {
             for (; j > 0; j--) {
                 // Here we look to go over any closing punctuation. This is because in dominant
                 // variants of English, the final period is placed within double quotes and maybe
@@ -195,7 +191,7 @@
         if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
         }
-        if (!isPeriod(c) || j <= 0) {
+        if (settingsValues.mSentenceSeparator != c || j <= 0) {
             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
         }
 
@@ -245,7 +241,7 @@
             case WORD:
                 if (Character.isLetter(c)) {
                     state = WORD;
-                } else if (isPeriod(c)) {
+                } else if (settingsValues.mSentenceSeparator == c) {
                     state = PERIOD;
                 } else {
                     return caps;
@@ -261,7 +257,7 @@
             case LETTER:
                 if (Character.isLetter(c)) {
                     state = LETTER;
-                } else if (isPeriod(c)) {
+                } else if (settingsValues.mSentenceSeparator == c) {
                     state = PERIOD;
                 } else {
                     return noCaps;
diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
new file mode 100644
index 0000000..ee2b97b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
@@ -0,0 +1,52 @@
+/*
+ * 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.dictionarypack.DictionarySettingsFragment;
+import com.android.inputmethod.latin.about.AboutPreferences;
+import com.android.inputmethod.latin.settings.AdditionalSubtypeSettings;
+import com.android.inputmethod.latin.settings.DebugSettings;
+import com.android.inputmethod.latin.settings.SettingsFragment;
+import com.android.inputmethod.latin.spellcheck.SpellCheckerSettingsFragment;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryAddWordFragment;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryLocalePicker;
+import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
+import com.android.inputmethod.research.FeedbackFragment;
+
+import java.util.HashSet;
+
+public class FragmentUtils {
+    private static final HashSet<String> sLatinImeFragments = new HashSet<String>();
+    static {
+        sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
+        sLatinImeFragments.add(AboutPreferences.class.getName());
+        sLatinImeFragments.add(AdditionalSubtypeSettings.class.getName());
+        sLatinImeFragments.add(DebugSettings.class.getName());
+        sLatinImeFragments.add(SettingsFragment.class.getName());
+        sLatinImeFragments.add(SpellCheckerSettingsFragment.class.getName());
+        sLatinImeFragments.add(UserDictionaryAddWordFragment.class.getName());
+        sLatinImeFragments.add(UserDictionaryList.class.getName());
+        sLatinImeFragments.add(UserDictionaryLocalePicker.class.getName());
+        sLatinImeFragments.add(UserDictionarySettings.class.getName());
+        sLatinImeFragments.add(FeedbackFragment.class.getName());
+    }
+
+    public static boolean isValidFragment(String fragmentName) {
+        return sLatinImeFragments.contains(fragmentName);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index ea32a74..635afe7 100644
--- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -49,7 +49,16 @@
     private static final String LAST_UPDATED_TIME_KEY = "date";
 
     public interface OnAddWordListener {
-        public void setUnigram(final String word, final String shortcutTarget, final int frequency);
+        /**
+         * Callback to be notified when a word is added to the dictionary.
+         * @param word The added word.
+         * @param shortcutTarget A shortcut target for this word, or null if none.
+         * @param frequency The frequency for this word.
+         * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist).
+         *   Unspecified if shortcutTarget is null - do not rely on its value.
+         */
+        public void setUnigram(final String word, final String shortcutTarget, final int frequency,
+                final int shortcutFreq);
         public void setBigram(final String word1, final String word2, final int frequency);
     }
 
@@ -153,7 +162,7 @@
         for (Entry<Integer, String> entry : unigrams.entrySet()) {
             final String word1 = entry.getValue();
             final int unigramFrequency = frequencies.get(entry.getKey());
-            to.setUnigram(word1, null, unigramFrequency);
+            to.setUnigram(word1, null /* shortcutTarget */, unigramFrequency, 0 /* shortcutFreq */);
             final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey());
             if (attrList != null) {
                 for (final PendingAttribute attr : attrList) {
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index 36afea5..ca6a779 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -85,8 +85,8 @@
     $(addprefix suggest/policyimpl/dictionary/utils/, \
         buffer_with_extendable_buffer.cpp \
         byte_array_utils.cpp \
-        decaying_utils.cpp \
         dict_file_writing_utils.cpp \
+        forgetting_curve_utils.cpp \
         format_utils.cpp) \
     suggest/policyimpl/gesture/gesture_suggest_policy_factory.cpp \
     $(addprefix suggest/policyimpl/typing/, \
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index c5ef264..8f21c50 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -67,7 +67,6 @@
         valueChars[valueUtf8Length] = '\0';
         HeaderReadWriteUtils::AttributeMap::mapped_type value;
         HeaderReadWriteUtils::insertCharactersIntoVector(valueChars, &value);
-
         attributeMap[key] = value;
     }
 
@@ -142,7 +141,7 @@
         jintArray inputCodePointsArray, jint inputSize, jint commitPoint, jintArray suggestOptions,
         jintArray prevWordCodePointsForBigrams, jintArray outputCodePointsArray,
         jintArray scoresArray, jintArray spaceIndicesArray, jintArray outputTypesArray,
-        jintArray outputAutoCommitFirstWordConfidence) {
+        jintArray outputAutoCommitFirstWordConfidenceArray) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return 0;
     ProximityInfo *pInfo = reinterpret_cast<ProximityInfo *>(proximityInfo);
@@ -196,17 +195,23 @@
     int spaceIndices[spaceIndicesLength];
     const jsize outputTypesLength = env->GetArrayLength(outputTypesArray);
     int outputTypes[outputTypesLength];
+    const jsize outputAutoCommitFirstWordConfidenceLength =
+            env->GetArrayLength(outputAutoCommitFirstWordConfidenceArray);
+    // We only use the first result, as obviously we will only ever autocommit the first one
+    ASSERT(outputAutoCommitFirstWordConfidenceLength == 1);
+    int outputAutoCommitFirstWordConfidence[outputAutoCommitFirstWordConfidenceLength];
     memset(outputCodePoints, 0, sizeof(outputCodePoints));
     memset(scores, 0, sizeof(scores));
     memset(spaceIndices, 0, sizeof(spaceIndices));
     memset(outputTypes, 0, sizeof(outputTypes));
+    memset(outputAutoCommitFirstWordConfidence, 0, sizeof(outputAutoCommitFirstWordConfidence));
 
     int count;
     if (givenSuggestOptions.isGesture() || inputSize > 0) {
         count = dictionary->getSuggestions(pInfo, traverseSession, xCoordinates, yCoordinates,
                 times, pointerIds, inputCodePoints, inputSize, prevWordCodePoints,
                 prevWordCodePointsLength, commitPoint, &givenSuggestOptions, outputCodePoints,
-                scores, spaceIndices, outputTypes);
+                scores, spaceIndices, outputTypes, outputAutoCommitFirstWordConfidence);
     } else {
         count = dictionary->getBigrams(prevWordCodePoints, prevWordCodePointsLength,
                 outputCodePoints, scores, outputTypes);
@@ -217,6 +222,8 @@
     env->SetIntArrayRegion(scoresArray, 0, scoresLength, scores);
     env->SetIntArrayRegion(spaceIndicesArray, 0, spaceIndicesLength, spaceIndices);
     env->SetIntArrayRegion(outputTypesArray, 0, outputTypesLength, outputTypes);
+    env->SetIntArrayRegion(outputAutoCommitFirstWordConfidenceArray, 0,
+            outputAutoCommitFirstWordConfidenceLength, outputAutoCommitFirstWordConfidence);
 
     return count;
 }
@@ -336,8 +343,7 @@
     static const int GET_PROPERTY_RESULT_LENGTH = 100;
     char resultChars[GET_PROPERTY_RESULT_LENGTH];
     resultChars[0] = '\0';
-    dictionary->getDictionaryStructurePolicy()->getProperty(queryChars, resultChars,
-            GET_PROPERTY_RESULT_LENGTH);
+    dictionary->getProperty(queryChars, resultChars, GET_PROPERTY_RESULT_LENGTH);
     return env->NewStringUTF(resultChars);
 }
 
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index c2aa8ba..742e388 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -299,6 +299,19 @@
 #define NOT_A_PROBABILITY (-1)
 #define NOT_A_DICT_POS (S_INT_MIN)
 
+// A special value to mean the first word confidence makes no sense in this case,
+// e.g. this is not a multi-word suggestion.
+#define NOT_A_FIRST_WORD_CONFIDENCE (S_INT_MAX)
+// How high the confidence needs to be for us to auto-commit. Arbitrary.
+// This needs to be the same as CONFIDENCE_FOR_AUTO_COMMIT in BinaryDictionary.java
+#define CONFIDENCE_FOR_AUTO_COMMIT (1000000)
+// 80% of the full confidence
+#define DISTANCE_WEIGHT_FOR_AUTO_COMMIT (80 * CONFIDENCE_FOR_AUTO_COMMIT / 100)
+// 100% of the full confidence
+#define LENGTH_WEIGHT_FOR_AUTO_COMMIT (CONFIDENCE_FOR_AUTO_COMMIT)
+// 80% of the full confidence
+#define SPACE_COUNT_WEIGHT_FOR_AUTO_COMMIT (80 * CONFIDENCE_FOR_AUTO_COMMIT / 100)
+
 #define KEYCODE_SPACE ' '
 #define KEYCODE_SINGLE_QUOTE '\''
 #define KEYCODE_HYPHEN_MINUS '-'
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 9099e82..49cfdec 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -271,7 +271,7 @@
         return isTerminalNodes && currentNodeDepth > 0 && currentNodeDepth == terminalNodeDepth;
     }
 
-    bool shouldBeFilterdBySafetyNetForBigram() const {
+    bool shouldBeFilteredBySafetyNetForBigram() const {
         const uint16_t currentDepth = getNodeCodePointCount();
         const int prevWordLen = mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength()
                 - mDicNodeState.mDicNodeStatePrevWord.getPrevWordStart() - 1;
@@ -321,6 +321,16 @@
         DUMP_WORD_AND_SCORE("OUTPUT");
     }
 
+    // "Total" in this context (and other methods in this class) means the whole suggestion. When
+    // this represents a multi-word suggestion, the referenced PtNode (in mDicNodeState) is only
+    // the one that corresponds to the last word of the suggestion, and all the previous words
+    // are concatenated together in mPrevWord - which contains a space at the end.
+    int getTotalNodeSpaceCount() const {
+        if (isFirstWord()) return 0;
+        return CharUtils::getSpaceCount(mDicNodeState.mDicNodeStatePrevWord.mPrevWord,
+                mDicNodeState.mDicNodeStatePrevWord.getPrevWordLength());
+    }
+
     int getSecondWordFirstInputIndex(const ProximityInfoState *const pInfoState) const {
         const int inputIndex = mDicNodeState.mDicNodeStatePrevWord.getSecondWordFirstInputIndex();
         if (inputIndex == NOT_AN_INDEX) {
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index b1d01ed..59ead18 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -55,14 +55,14 @@
         int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
         int inputSize, int *prevWordCodePoints, int prevWordLength, int commitPoint,
         const SuggestOptions *const suggestOptions, int *outWords, int *frequencies,
-        int *spaceIndices, int *outputTypes) const {
+        int *spaceIndices, int *outputTypes, int *outputAutoCommitFirstWordConfidence) const {
     int result = 0;
     if (suggestOptions->isGesture()) {
         DicTraverseSession::initSessionInstance(
                 traverseSession, this, prevWordCodePoints, prevWordLength, suggestOptions);
         result = mGestureSuggest->getSuggestions(proximityInfo, traverseSession, xcoordinates,
                 ycoordinates, times, pointerIds, inputCodePoints, inputSize, commitPoint, outWords,
-                frequencies, spaceIndices, outputTypes);
+                frequencies, spaceIndices, outputTypes, outputAutoCommitFirstWordConfidence);
         if (DEBUG_DICT) {
             DUMP_RESULT(outWords, frequencies);
         }
@@ -72,7 +72,8 @@
                 traverseSession, this, prevWordCodePoints, prevWordLength, suggestOptions);
         result = mTypingSuggest->getSuggestions(proximityInfo, traverseSession, xcoordinates,
                 ycoordinates, times, pointerIds, inputCodePoints, inputSize, commitPoint,
-                outWords, frequencies, spaceIndices, outputTypes);
+                outWords, frequencies, spaceIndices, outputTypes,
+                outputAutoCommitFirstWordConfidence);
         if (DEBUG_DICT) {
             DUMP_RESULT(outWords, frequencies);
         }
@@ -128,7 +129,7 @@
 }
 
 void Dictionary::getProperty(const char *const query, char *const outResult,
-        const int maxResultLength) const {
+        const int maxResultLength) {
     return mDictionaryStructureWithBufferPolicy->getProperty(query, outResult, maxResultLength);
 }
 
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index d8a0f3e..0195d5b 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -60,7 +60,7 @@
             int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
             int inputSize, int *prevWordCodePoints, int prevWordLength, int commitPoint,
             const SuggestOptions *const suggestOptions, int *outWords, int *frequencies,
-            int *spaceIndices, int *outputTypes) const;
+            int *spaceIndices, int *outputTypes, int *outputAutoCommitFirstWordConfidence) const;
 
     int getBigrams(const int *word, int length, int *outWords, int *frequencies,
             int *outputTypes) const;
@@ -84,7 +84,7 @@
     bool needsToRunGC(const bool mindsBlockByGC);
 
     void getProperty(const char *const query, char *const outResult,
-            const int maxResultLength) const;
+            const int maxResultLength);
 
     const DictionaryStructureWithBufferPolicy *getDictionaryStructurePolicy() const {
         return mDictionaryStructureWithBufferPolicy;
diff --git a/native/jni/src/suggest/core/dictionary/shortcut_utils.h b/native/jni/src/suggest/core/dictionary/shortcut_utils.h
index 461d7b4..9ccef02 100644
--- a/native/jni/src/suggest/core/dictionary/shortcut_utils.h
+++ b/native/jni/src/suggest/core/dictionary/shortcut_utils.h
@@ -44,7 +44,7 @@
                 shortcutScore = finalScore;
                 // Protection against int underflow
                 shortcutScore = max(S_INT_MIN + 1, shortcutScore) - 1;
-                kind = Dictionary::KIND_CORRECTION;
+                kind = Dictionary::KIND_SHORTCUT;
             }
             outputTypes[outputWordIndex] = kind;
             frequencies[outputWordIndex] = shortcutScore;
diff --git a/native/jni/src/suggest/core/layout/proximity_info_params.cpp b/native/jni/src/suggest/core/layout/proximity_info_params.cpp
index 0e887f7..49df103 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_params.cpp
+++ b/native/jni/src/suggest/core/layout/proximity_info_params.cpp
@@ -69,13 +69,13 @@
 const float ProximityInfoParams::SKIP_CORNER_PROBABILITY = 0.4f;
 const float ProximityInfoParams::SPEED_MARGIN = 0.1f;
 const float ProximityInfoParams::CENTER_VALUE_OF_NORMALIZED_DISTRIBUTION = 0.0f;
-// TODO: The variance is critical for accuracy; thus, adjusting these parameter by machine
+// TODO: The variance is critical for accuracy; thus, adjusting these parameters by machine
 // learning or something would be efficient.
-const float ProximityInfoParams::SPEEDxANGLE_WEIGHT_FOR_STANDARD_DIVIATION = 0.3f;
-const float ProximityInfoParams::MAX_SPEEDxANGLE_RATE_FOR_STANDERD_DIVIATION = 0.25f;
-const float ProximityInfoParams::SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DIVIATION = 0.5f;
-const float ProximityInfoParams::MAX_SPEEDxNEAREST_RATE_FOR_STANDERD_DIVIATION = 0.15f;
-const float ProximityInfoParams::MIN_STANDERD_DIVIATION = 0.37f;
+const float ProximityInfoParams::SPEEDxANGLE_WEIGHT_FOR_STANDARD_DEVIATION = 0.3f;
+const float ProximityInfoParams::MAX_SPEEDxANGLE_RATE_FOR_STANDARD_DEVIATION = 0.25f;
+const float ProximityInfoParams::SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DEVIATION = 0.5f;
+const float ProximityInfoParams::MAX_SPEEDxNEAREST_RATE_FOR_STANDARD_DEVIATION = 0.15f;
+const float ProximityInfoParams::MIN_STANDARD_DEVIATION = 0.37f;
 const float ProximityInfoParams::PREV_DISTANCE_WEIGHT = 0.5f;
 const float ProximityInfoParams::NEXT_DISTANCE_WEIGHT = 0.6f;
 
diff --git a/native/jni/src/suggest/core/layout/proximity_info_params.h b/native/jni/src/suggest/core/layout/proximity_info_params.h
index 4e47f73..ae1f82c 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_params.h
+++ b/native/jni/src/suggest/core/layout/proximity_info_params.h
@@ -73,11 +73,11 @@
     static const float SKIP_CORNER_PROBABILITY;
     static const float SPEED_MARGIN;
     static const float CENTER_VALUE_OF_NORMALIZED_DISTRIBUTION;
-    static const float SPEEDxANGLE_WEIGHT_FOR_STANDARD_DIVIATION;
-    static const float MAX_SPEEDxANGLE_RATE_FOR_STANDERD_DIVIATION;
-    static const float SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DIVIATION;
-    static const float MAX_SPEEDxNEAREST_RATE_FOR_STANDERD_DIVIATION;
-    static const float MIN_STANDERD_DIVIATION;
+    static const float SPEEDxANGLE_WEIGHT_FOR_STANDARD_DEVIATION;
+    static const float MAX_SPEEDxANGLE_RATE_FOR_STANDARD_DEVIATION;
+    static const float SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DEVIATION;
+    static const float MAX_SPEEDxNEAREST_RATE_FOR_STANDARD_DEVIATION;
+    static const float MIN_STANDARD_DEVIATION;
     static const float PREV_DISTANCE_WEIGHT;
     static const float NEXT_DISTANCE_WEIGHT;
 
diff --git a/native/jni/src/suggest/core/layout/proximity_info_state_utils.cpp b/native/jni/src/suggest/core/layout/proximity_info_state_utils.cpp
index 904671f..e1b3534 100644
--- a/native/jni/src/suggest/core/layout/proximity_info_state_utils.cpp
+++ b/native/jni/src/suggest/core/layout/proximity_info_state_utils.cpp
@@ -708,13 +708,13 @@
         const float inputCharProbability = 1.0f - skipProbability;
 
         const float speedxAngleRate = min(speedRate * currentAngle / M_PI_F
-                * ProximityInfoParams::SPEEDxANGLE_WEIGHT_FOR_STANDARD_DIVIATION,
-                        ProximityInfoParams::MAX_SPEEDxANGLE_RATE_FOR_STANDERD_DIVIATION);
+                * ProximityInfoParams::SPEEDxANGLE_WEIGHT_FOR_STANDARD_DEVIATION,
+                        ProximityInfoParams::MAX_SPEEDxANGLE_RATE_FOR_STANDARD_DEVIATION);
         const float speedxNearestKeyDistanceRate = min(speedRate * nearestKeyDistance
-                * ProximityInfoParams::SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DIVIATION,
-                        ProximityInfoParams::MAX_SPEEDxNEAREST_RATE_FOR_STANDERD_DIVIATION);
+                * ProximityInfoParams::SPEEDxNEAREST_WEIGHT_FOR_STANDARD_DEVIATION,
+                        ProximityInfoParams::MAX_SPEEDxNEAREST_RATE_FOR_STANDARD_DEVIATION);
         const float sigma = speedxAngleRate + speedxNearestKeyDistanceRate
-                + ProximityInfoParams::MIN_STANDERD_DIVIATION;
+                + ProximityInfoParams::MIN_STANDARD_DEVIATION;
 
         ProximityInfoUtils::NormalDistribution
                 distribution(ProximityInfoParams::CENTER_VALUE_OF_NORMALIZED_DISTRIBUTION, sigma);
diff --git a/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h b/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
index a6829b4..5492c60 100644
--- a/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
+++ b/native/jni/src/suggest/core/policy/dictionary_header_structure_policy.h
@@ -37,6 +37,8 @@
 
     virtual float getMultiWordCostMultiplier() const = 0;
 
+    virtual int getLastDecayedTime() const = 0;
+
     virtual void readHeaderValueOrQuestionMark(const char *const key, int *outValue,
             int outValueSize) const = 0;
 
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 c7ffef0..41f8204 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
@@ -80,8 +80,10 @@
 
     virtual bool needsToRunGC(const bool mindsBlockByGC) const = 0;
 
+    // Currently, this method is used only for testing. You may want to consider creating new
+    // dedicated method instead of this if you want to use this in the production.
     virtual void getProperty(const char *const query, char *const outResult,
-            const int maxResultLength) const = 0;
+            const int maxResultLength) = 0;
 
  protected:
     DictionaryStructureWithBufferPolicy() {}
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index e20bc49..73ccebc 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -49,7 +49,7 @@
 int Suggest::getSuggestions(ProximityInfo *pInfo, void *traverseSession,
         int *inputXs, int *inputYs, int *times, int *pointerIds, int *inputCodePoints,
         int inputSize, int commitPoint, int *outWords, int *frequencies, int *outputIndices,
-        int *outputTypes) const {
+        int *outputTypes, int *outputAutoCommitFirstWordConfidence) const {
     PROF_OPEN;
     PROF_START(0);
     const float maxSpatialDistance = TRAVERSAL->getMaxSpatialDistance();
@@ -70,7 +70,8 @@
     }
     PROF_END(1);
     PROF_START(2);
-    const int size = outputSuggestions(tSession, frequencies, outWords, outputIndices, outputTypes);
+    const int size = outputSuggestions(tSession, frequencies, outWords, outputIndices, outputTypes,
+            outputAutoCommitFirstWordConfidence);
     PROF_END(2);
     PROF_CLOSE;
     return size;
@@ -117,7 +118,8 @@
  * Outputs the final list of suggestions (i.e., terminal nodes).
  */
 int Suggest::outputSuggestions(DicTraverseSession *traverseSession, int *frequencies,
-        int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes) const {
+        int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes,
+        int *outputAutoCommitFirstWordConfidence) const {
 #if DEBUG_EVALUATE_MOST_PROBABLE_STRING
     const int terminalSize = 0;
 #else
@@ -164,6 +166,12 @@
     // TODO: have partial commit work even with multiple pointers.
     const bool outputSecondWordFirstLetterInputIndex =
             traverseSession->isOnlyOnePointerUsed(0 /* pointerId */);
+    if (terminalSize > 0) {
+        // If we have no suggestions, don't write this
+        outputAutoCommitFirstWordConfidence[0] =
+                computeFirstWordConfidence(&terminals[0]);
+    }
+
     // Output suggestion results here
     for (int terminalIndex = 0; terminalIndex < terminalSize && outputWordIndex < MAX_RESULTS;
             ++terminalIndex) {
@@ -251,6 +259,57 @@
     return outputWordIndex;
 }
 
+int Suggest::computeFirstWordConfidence(const DicNode *const terminalDicNode) const {
+    // Get the number of spaces in the first suggestion
+    const int spaceCount = terminalDicNode->getTotalNodeSpaceCount();
+    // Get the number of characters in the first suggestion
+    const int length = terminalDicNode->getTotalNodeCodePointCount();
+    // Get the distance for the first word of the suggestion
+    const float distance = terminalDicNode->getNormalizedCompoundDistanceAfterFirstWord();
+
+    // Arbitrarily, we give a score whose useful values range from 0 to 1,000,000.
+    // 1,000,000 will be the cutoff to auto-commit. It's fine if the number is under 0 or
+    // above 1,000,000 : under 0 just means it's very bad to commit, and above 1,000,000 means
+    // we are very confident.
+    // Expected space count is 1 ~ 5
+    static const int MIN_EXPECTED_SPACE_COUNT = 1;
+    static const int MAX_EXPECTED_SPACE_COUNT = 5;
+    // Expected length is about 4 ~ 30
+    static const int MIN_EXPECTED_LENGTH = 4;
+    static const int MAX_EXPECTED_LENGTH = 30;
+    // Expected distance is about 0.2 ~ 2.0, but consider 0.0 ~ 2.0
+    static const float MIN_EXPECTED_DISTANCE = 0.0;
+    static const float MAX_EXPECTED_DISTANCE = 2.0;
+    // This is not strict: it's where most stuff will be falling, but it's still fine if it's
+    // outside these values. We want to output a value that reflects all of these. Each factor
+    // contributes a bit.
+
+    // We need at least a space.
+    if (spaceCount < 1) return NOT_A_FIRST_WORD_CONFIDENCE;
+
+    // The smaller the edit distance, the higher the contribution. MIN_EXPECTED_DISTANCE means 0
+    // contribution, while MAX_EXPECTED_DISTANCE means full contribution according to the
+    // weight of the distance. Clamp to avoid overflows.
+    const float clampedDistance = distance < MIN_EXPECTED_DISTANCE ? MIN_EXPECTED_DISTANCE
+            : distance > MAX_EXPECTED_DISTANCE ? MAX_EXPECTED_DISTANCE : distance;
+    const int distanceContribution = DISTANCE_WEIGHT_FOR_AUTO_COMMIT
+            * (MAX_EXPECTED_DISTANCE - clampedDistance)
+            / (MAX_EXPECTED_DISTANCE - MIN_EXPECTED_DISTANCE);
+    // The larger the suggestion length, the larger the contribution. MIN_EXPECTED_LENGTH is no
+    // contribution, MAX_EXPECTED_LENGTH is full contribution according to the weight of the
+    // length. Length is guaranteed to be between 1 and 48, so we don't need to clamp.
+    const int lengthContribution = LENGTH_WEIGHT_FOR_AUTO_COMMIT
+            * (length - MIN_EXPECTED_LENGTH) / (MAX_EXPECTED_LENGTH - MIN_EXPECTED_LENGTH);
+    // The more spaces, the larger the contribution. MIN_EXPECTED_SPACE_COUNT space is no
+    // contribution, MAX_EXPECTED_SPACE_COUNT spaces is full contribution according to the
+    // weight of the space count.
+    const int spaceContribution = SPACE_COUNT_WEIGHT_FOR_AUTO_COMMIT
+            * (spaceCount - MIN_EXPECTED_SPACE_COUNT)
+            / (MAX_EXPECTED_SPACE_COUNT - MIN_EXPECTED_SPACE_COUNT);
+
+    return distanceContribution + lengthContribution + spaceContribution;
+}
+
 /**
  * Expands the dicNodes in the current search priority queue by advancing to the possible child
  * nodes based on the next touch point(s) (or no touch points for lookahead)
@@ -386,7 +445,7 @@
     if (!dicNode->isTerminalWordNode()) {
         return;
     }
-    if (dicNode->shouldBeFilterdBySafetyNetForBigram()) {
+    if (dicNode->shouldBeFilteredBySafetyNetForBigram()) {
         return;
     }
     // Create a non-cached node here.
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index b240196..b20343d 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -48,14 +48,17 @@
     AK_FORCE_INLINE virtual ~Suggest() {}
     int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
             int *times, int *pointerIds, int *inputCodePoints, int inputSize, int commitPoint,
-            int *outWords, int *frequencies, int *outputIndices, int *outputTypes) const;
+            int *outWords, int *frequencies, int *outputIndices, int *outputTypes,
+            int *outputAutoCommitFirstWordConfidence) const;
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Suggest);
     void createNextWordDicNode(DicTraverseSession *traverseSession, DicNode *dicNode,
             const bool spaceSubstitution) const;
     int outputSuggestions(DicTraverseSession *traverseSession, int *frequencies,
-            int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes) const;
+            int *outputCodePoints, int *outputIndicesToPartialCommit, int *outputTypes,
+            int *outputAutoCommitFirstWordConfidence) const;
+    int computeFirstWordConfidence(const DicNode *const terminalDicNode) const;
     void initializeSearch(DicTraverseSession *traverseSession, int commitPoint) const;
     void expandCurrentDicNodes(DicTraverseSession *traverseSession) const;
     void processTerminalDicNode(DicTraverseSession *traverseSession, DicNode *dicNode) const;
diff --git a/native/jni/src/suggest/core/suggest_interface.h b/native/jni/src/suggest/core/suggest_interface.h
index 0bb85d7..4deb4d9 100644
--- a/native/jni/src/suggest/core/suggest_interface.h
+++ b/native/jni/src/suggest/core/suggest_interface.h
@@ -28,7 +28,7 @@
     virtual int getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs,
             int *inputYs, int *times, int *pointerIds, int *inputCodePoints, int inputSize,
             int commitPoint, int *outWords, int *frequencies, int *outputIndices,
-            int *outputTypes) const = 0;
+            int *outputTypes, int *outputAutoCommitFirstWordConfidence) const = 0;
     SuggestInterface() {}
     virtual ~SuggestInterface() {}
  private:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
index 67a085d..b1170e2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.cpp
@@ -20,7 +20,7 @@
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_node_reader.h"
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h"
 #include "suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.h"
-#include "suggest/policyimpl/dictionary/utils/decaying_utils.h"
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
 
 namespace latinime {
 
@@ -43,7 +43,7 @@
     }
     *outProbability = BigramListReadWriteUtils::getProbabilityFromFlags(bigramFlags);
     *outHasNext = BigramListReadWriteUtils::hasNext(bigramFlags);
-    if (mIsDecayingDict && !DecayingUtils::isValidBigram(*outProbability)) {
+    if (mIsDecayingDict && !ForgettingCurveUtils::isValidEncodedProbability(*outProbability)) {
         // This bigram is too weak to output.
         *outBigramPos = NOT_A_DICT_POS;
     } else {
@@ -261,8 +261,8 @@
             const int originalProbability = BigramListReadWriteUtils::getProbabilityFromFlags(
                     bigramFlags);
             const int probabilityToWrite = mIsDecayingDict ?
-                    DecayingUtils::getUpdatedBigramProbabilityDelta(
-                            originalProbability, probability) : probability;
+                    ForgettingCurveUtils::getUpdatedEncodedProbability(originalProbability,
+                            probability) : probability;
             const BigramListReadWriteUtils::BigramFlags updatedFlags =
                     BigramListReadWriteUtils::setProbabilityInFlags(bigramFlags,
                             probabilityToWrite);
@@ -294,7 +294,7 @@
         int *const writingPos) {
     // hasNext is false because we are adding a new bigram entry at the end of the bigram list.
     const int probabilityToWrite = mIsDecayingDict ?
-            DecayingUtils::getUpdatedBigramProbabilityDelta(NOT_A_PROBABILITY, probability) :
+            ForgettingCurveUtils::getUpdatedEncodedProbability(NOT_A_PROBABILITY, probability) :
                     probability;
     return BigramListReadWriteUtils::createAndWriteBigramEntry(mBuffer, bigramTargetPos,
             probabilityToWrite, false /* hasNext */, writingPos);
@@ -360,14 +360,14 @@
 }
 
 bool DynamicBigramListPolicy::updateProbabilityForDecay(
-        BigramListReadWriteUtils::BigramFlags bigramFlags, const int targetPtNodePos,
+        const BigramListReadWriteUtils::BigramFlags bigramFlags, const int targetPtNodePos,
         int *const bigramEntryPos, bool *const outRemoved) const {
     *outRemoved = false;
     if (mIsDecayingDict) {
         // Update bigram probability for decaying.
-        const int newProbability = DecayingUtils::getBigramProbabilityDeltaToSave(
-                BigramListReadWriteUtils::getProbabilityFromFlags(bigramFlags));
-        if (DecayingUtils::isValidBigram(newProbability)) {
+        const int newProbability = ForgettingCurveUtils::getEncodedProbabilityToSave(
+                BigramListReadWriteUtils::getProbabilityFromFlags(bigramFlags), mHeaderPolicy);
+        if (ForgettingCurveUtils::isValidEncodedProbability(newProbability)) {
             // Write new probability.
             const BigramListReadWriteUtils::BigramFlags updatedBigramFlags =
                     BigramListReadWriteUtils::setProbabilityInFlags(
diff --git a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
index b358b4e..0504b59 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/bigram/dynamic_bigram_list_policy.h
@@ -27,6 +27,7 @@
 namespace latinime {
 
 class BufferWithExtendableBuffer;
+class DictionaryHeaderStructurePolicy;
 class DictionaryShortcutsStructurePolicy;
 
 /*
@@ -34,10 +35,12 @@
  */
 class DynamicBigramListPolicy : public DictionaryBigramsStructurePolicy {
  public:
-    DynamicBigramListPolicy(BufferWithExtendableBuffer *const buffer,
+    DynamicBigramListPolicy(const DictionaryHeaderStructurePolicy *const headerPolicy,
+            BufferWithExtendableBuffer *const buffer,
             const DictionaryShortcutsStructurePolicy *const shortcutPolicy,
             const bool isDecayingDict)
-            : mBuffer(buffer), mShortcutPolicy(shortcutPolicy), mIsDecayingDict(isDecayingDict) {}
+            : mHeaderPolicy(headerPolicy), mBuffer(buffer), mShortcutPolicy(shortcutPolicy),
+              mIsDecayingDict(isDecayingDict) {}
 
     ~DynamicBigramListPolicy() {}
 
@@ -74,6 +77,7 @@
     static const int CONTINUING_BIGRAM_LINK_COUNT_LIMIT;
     static const int BIGRAM_ENTRY_COUNT_IN_A_BIGRAM_LIST_LIMIT;
 
+    const DictionaryHeaderStructurePolicy *const mHeaderPolicy;
     BufferWithExtendableBuffer *const mBuffer;
     const DictionaryShortcutsStructurePolicy *const mShortcutPolicy;
     const bool mIsDecayingDict;
@@ -81,7 +85,7 @@
     // Follow bigram link and return the position of bigram target PtNode that is currently valid.
     int followBigramLinkAndGetCurrentBigramPtNodePos(const int originalBigramPos) const;
 
-    bool updateProbabilityForDecay(BigramListReadWriteUtils::BigramFlags bigramFlags,
+    bool updateProbabilityForDecay(const BigramListReadWriteUtils::BigramFlags bigramFlags,
             const int targetPtNodePos, int *const bigramEntryPos, bool *const outRemoved) const;
 };
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.cpp
index 081163a..5724c5d 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.cpp
@@ -16,7 +16,8 @@
 
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.h"
 
-#include "suggest/policyimpl/dictionary/utils/decaying_utils.h"
+#include "suggest/core/policy/dictionary_header_structure_policy.h"
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
 
 namespace latinime {
 
@@ -29,15 +30,16 @@
     bool isUselessPtNode = !node->isTerminal();
     if (node->isTerminal() && mIsDecayingDict) {
         const int newProbability =
-                DecayingUtils::getUnigramProbabilityToSave(node->getProbability());
+                ForgettingCurveUtils::getEncodedProbabilityToSave(node->getProbability(),
+                        mHeaderPolicy);
         int writingPos = node->getProbabilityFieldPos();
         // Update probability.
         if (!DynamicPatriciaTrieWritingUtils::writeProbabilityAndAdvancePosition(
                 mBuffer, newProbability, &writingPos)) {
             return false;
         }
-        if (!DecayingUtils::isValidUnigram(newProbability)) {
-            isUselessPtNode = false;
+        if (!ForgettingCurveUtils::isValidEncodedProbability(newProbability)) {
+            isUselessPtNode = true;
         }
     }
     if (mChildrenValue > 0) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.h
index 463715a..9755120 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_gc_event_listeners.h
@@ -29,6 +29,8 @@
 
 namespace latinime {
 
+class DictionaryHeaderStructurePolicy;
+
 class DynamicPatriciaTrieGcEventListeners {
  public:
     // Updates all PtNodes that can be reached from the root. Checks if each PtNode is useless or
@@ -38,10 +40,12 @@
         : public DynamicPatriciaTrieReadingHelper::TraversingEventListener {
      public:
         TraversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted(
+                const DictionaryHeaderStructurePolicy *const headerPolicy,
                 DynamicPatriciaTrieWritingHelper *const writingHelper,
                 BufferWithExtendableBuffer *const buffer, const bool isDecayingDict)
-                : mWritingHelper(writingHelper), mBuffer(buffer), mIsDecayingDict(isDecayingDict),
-                  mValueStack(), mChildrenValue(0), mValidUnigramCount(0) {}
+                : mHeaderPolicy(headerPolicy), mWritingHelper(writingHelper), mBuffer(buffer),
+                  mIsDecayingDict(isDecayingDict), mValueStack(), mChildrenValue(0),
+                  mValidUnigramCount(0) {}
 
         ~TraversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted() {};
 
@@ -56,6 +60,7 @@
 
         bool onDescend(const int ptNodeArrayPos) {
             mValueStack.push_back(0);
+            mChildrenValue = 0;
             return true;
         }
 
@@ -72,9 +77,10 @@
         DISALLOW_IMPLICIT_CONSTRUCTORS(
                 TraversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted);
 
+        const DictionaryHeaderStructurePolicy *const mHeaderPolicy;
         DynamicPatriciaTrieWritingHelper *const mWritingHelper;
         BufferWithExtendableBuffer *const mBuffer;
-        const int mIsDecayingDict;
+        const bool mIsDecayingDict;
         std::vector<int> mValueStack;
         int mChildrenValue;
         int mValidUnigramCount;
@@ -85,7 +91,8 @@
     class TraversePolicyToUpdateBigramProbability
             : public DynamicPatriciaTrieReadingHelper::TraversingEventListener {
      public:
-        TraversePolicyToUpdateBigramProbability(DynamicBigramListPolicy *const bigramPolicy)
+        TraversePolicyToUpdateBigramProbability(
+                DynamicBigramListPolicy *const bigramPolicy)
                 : mBigramPolicy(bigramPolicy), mValidBigramEntryCount(0) {}
 
         bool onAscend() { return true; }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
index 0d8c927..a8ea69f 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.cpp
@@ -28,17 +28,22 @@
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_utils.h"
 #include "suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h"
 #include "suggest/policyimpl/dictionary/patricia_trie_reading_utils.h"
-#include "suggest/policyimpl/dictionary/utils/decaying_utils.h"
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
 #include "suggest/policyimpl/dictionary/utils/probability_utils.h"
 
 namespace latinime {
 
+// Note that these are corresponding definitions in Java side in BinaryDictionaryTests and
+// BinaryDictionaryDecayingTests.
 const char *const DynamicPatriciaTriePolicy::UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT";
 const char *const DynamicPatriciaTriePolicy::BIGRAM_COUNT_QUERY = "BIGRAM_COUNT";
+const char *const DynamicPatriciaTriePolicy::MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT";
+const char *const DynamicPatriciaTriePolicy::MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT";
+const char *const DynamicPatriciaTriePolicy::SET_NEEDS_TO_DECAY_FOR_TESTING_QUERY =
+        "SET_NEEDS_TO_DECAY_FOR_TESTING";
 const int DynamicPatriciaTriePolicy::MAX_DICT_EXTENDED_REGION_SIZE = 1024 * 1024;
 const int DynamicPatriciaTriePolicy::MIN_DICT_SIZE_TO_REFUSE_DYNAMIC_OPERATIONS =
         DynamicPatriciaTrieWritingHelper::MAX_DICTIONARY_SIZE - 1024;
-const int DynamicPatriciaTriePolicy::MIN_SECONDS_TO_REQUIRE_GC_WHEN_WRITING = 2 * 60 * 60;
 
 void DynamicPatriciaTriePolicy::createAndGetAllChildNodes(const DicNode *const dicNode,
         DicNodeVector *const childDicNodes) const {
@@ -150,7 +155,7 @@
 int DynamicPatriciaTriePolicy::getProbability(const int unigramProbability,
         const int bigramProbability) const {
     if (mHeaderPolicy.isDecayingDict()) {
-        return DecayingUtils::getProbability(unigramProbability, bigramProbability);
+        return ForgettingCurveUtils::getProbability(unigramProbability, bigramProbability);
     } else {
         if (unigramProbability == NOT_A_PROBABILITY) {
             return NOT_A_PROBABILITY;
@@ -301,7 +306,7 @@
         return;
     }
     DynamicPatriciaTrieWritingHelper writingHelper(&mBufferWithExtendableBuffer,
-            &mBigramListPolicy, &mShortcutListPolicy, mHeaderPolicy.isDecayingDict());
+            &mBigramListPolicy, &mShortcutListPolicy, false /* needsToDecay */);
     writingHelper.writeToDictFile(filePath, &mHeaderPolicy, mUnigramCount, mBigramCount);
 }
 
@@ -310,9 +315,15 @@
         AKLOGI("Warning: flushWithGC() is called for non-updatable dictionary.");
         return;
     }
+    const bool needsToDecay = mHeaderPolicy.isDecayingDict()
+            && (mNeedsToDecayForTesting || ForgettingCurveUtils::needsToDecay(
+                    false /* mindsBlockByDecay */, mUnigramCount, mBigramCount, &mHeaderPolicy));
+    DynamicBigramListPolicy bigramListPolicyForGC(&mHeaderPolicy, &mBufferWithExtendableBuffer,
+            &mShortcutListPolicy, needsToDecay);
     DynamicPatriciaTrieWritingHelper writingHelper(&mBufferWithExtendableBuffer,
-            &mBigramListPolicy, &mShortcutListPolicy, mHeaderPolicy.isDecayingDict());
+            &bigramListPolicyForGC, &mShortcutListPolicy, needsToDecay);
     writingHelper.writeToDictFileWithGC(getRootPosition(), filePath, &mHeaderPolicy);
+    mNeedsToDecayForTesting = false;
 }
 
 bool DynamicPatriciaTriePolicy::needsToRunGC(const bool mindsBlockByGC) const {
@@ -334,27 +345,28 @@
         // Needs to reduce dictionary size.
         return true;
     } else if (mHeaderPolicy.isDecayingDict()) {
-        if (mUnigramCount >= DecayingUtils::MAX_UNIGRAM_COUNT) {
-            // Unigram count exceeds the limit.
-            return true;
-        } else if (mBigramCount >= DecayingUtils::MAX_BIGRAM_COUNT) {
-            // Bigram count exceeds the limit.
-            return true;
-        } else if (mindsBlockByGC && mHeaderPolicy.getLastUpdatedTime()
-                + MIN_SECONDS_TO_REQUIRE_GC_WHEN_WRITING < time(0)) {
-            // Time to update probabilities for decaying.
-            return true;
-        }
+        return mNeedsToDecayForTesting || ForgettingCurveUtils::needsToDecay(
+                mindsBlockByGC, mUnigramCount, mBigramCount, &mHeaderPolicy);
     }
     return false;
 }
 
 void DynamicPatriciaTriePolicy::getProperty(const char *const query, char *const outResult,
-        const int maxResultLength) const {
+        const int maxResultLength) {
     if (strncmp(query, UNIGRAM_COUNT_QUERY, maxResultLength) == 0) {
         snprintf(outResult, maxResultLength, "%d", mUnigramCount);
     } else if (strncmp(query, BIGRAM_COUNT_QUERY, maxResultLength) == 0) {
         snprintf(outResult, maxResultLength, "%d", mBigramCount);
+    } else if (strncmp(query, MAX_UNIGRAM_COUNT_QUERY, maxResultLength) == 0) {
+        snprintf(outResult, maxResultLength, "%d",
+                mHeaderPolicy.isDecayingDict() ? ForgettingCurveUtils::MAX_UNIGRAM_COUNT :
+                        static_cast<int>(DynamicPatriciaTrieWritingHelper::MAX_DICTIONARY_SIZE));
+    } else if (strncmp(query, MAX_BIGRAM_COUNT_QUERY, maxResultLength) == 0) {
+        snprintf(outResult, maxResultLength, "%d",
+                mHeaderPolicy.isDecayingDict() ? ForgettingCurveUtils::MAX_BIGRAM_COUNT :
+                        static_cast<int>(DynamicPatriciaTrieWritingHelper::MAX_DICTIONARY_SIZE));
+    } else if (strncmp(query, SET_NEEDS_TO_DECAY_FOR_TESTING_QUERY, maxResultLength) == 0) {
+        mNeedsToDecayForTesting = true;
     }
 }
 
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
index d3150c6..be97ee1 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_policy.h
@@ -37,10 +37,10 @@
               mBufferWithExtendableBuffer(mBuffer->getBuffer() + mHeaderPolicy.getSize(),
                       mBuffer->getBufferSize() - mHeaderPolicy.getSize()),
               mShortcutListPolicy(&mBufferWithExtendableBuffer),
-              mBigramListPolicy(&mBufferWithExtendableBuffer, &mShortcutListPolicy,
+              mBigramListPolicy(&mHeaderPolicy, &mBufferWithExtendableBuffer, &mShortcutListPolicy,
                       mHeaderPolicy.isDecayingDict()),
               mUnigramCount(mHeaderPolicy.getUnigramCount()),
-              mBigramCount(mHeaderPolicy.getBigramCount()) {}
+              mBigramCount(mHeaderPolicy.getBigramCount()), mNeedsToDecayForTesting(false) {}
 
     ~DynamicPatriciaTriePolicy() {
         delete mBuffer;
@@ -95,16 +95,18 @@
     bool needsToRunGC(const bool mindsBlockByGC) const;
 
     void getProperty(const char *const query, char *const outResult,
-            const int maxResultLength) const;
+            const int maxResultLength);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DynamicPatriciaTriePolicy);
 
-    static const char*const UNIGRAM_COUNT_QUERY;
-    static const char*const BIGRAM_COUNT_QUERY;
+    static const char *const UNIGRAM_COUNT_QUERY;
+    static const char *const BIGRAM_COUNT_QUERY;
+    static const char *const MAX_UNIGRAM_COUNT_QUERY;
+    static const char *const MAX_BIGRAM_COUNT_QUERY;
+    static const char *const SET_NEEDS_TO_DECAY_FOR_TESTING_QUERY;
     static const int MAX_DICT_EXTENDED_REGION_SIZE;
     static const int MIN_DICT_SIZE_TO_REFUSE_DYNAMIC_OPERATIONS;
-    static const int MIN_SECONDS_TO_REQUIRE_GC_WHEN_WRITING;
 
     const MmappedBuffer *const mBuffer;
     const HeaderPolicy mHeaderPolicy;
@@ -113,6 +115,7 @@
     DynamicBigramListPolicy mBigramListPolicy;
     int mUnigramCount;
     int mBigramCount;
+    int mNeedsToDecayForTesting;
 };
 } // namespace latinime
 #endif // LATINIME_DYNAMIC_PATRICIA_TRIE_POLICY_H
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.cpp
index 601ee66..f108c21 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.cpp
@@ -93,6 +93,12 @@
     if (!listener->onDescend(getPosOfLastPtNodeArrayHead())) {
         return false;
     }
+    if (isEnd()) {
+        // Empty dictionary. Needs to notify the listener of the tail of empty PtNode array.
+        if (!listener->onReadingPtNodeArrayTail()) {
+            return false;
+        }
+    }
     pushReadingStateToStack();
     while (!isEnd()) {
         if (alreadyVisitedAllPtNodesInArray) {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
index 512a4d8..a71c069 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_reading_helper.h
@@ -279,7 +279,9 @@
         } else {
             mReadingState = mReadingStateStack.back();
             mReadingStateStack.pop_back();
-            fetchPtNodeInfo();
+            if (!isEnd()) {
+                fetchPtNodeInfo();
+            }
         }
     }
 };
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
index 28124d2..052558b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.cpp
@@ -25,8 +25,8 @@
 #include "suggest/policyimpl/dictionary/header/header_policy.h"
 #include "suggest/policyimpl/dictionary/patricia_trie_reading_utils.h"
 #include "suggest/policyimpl/dictionary/shortcut/dynamic_shortcut_list_policy.h"
-#include "suggest/policyimpl/dictionary/utils/decaying_utils.h"
 #include "suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h"
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
 #include "utils/hash_map_compat.h"
 
 namespace latinime {
@@ -153,7 +153,7 @@
     const int extendedRegionSize = headerPolicy->getExtendedRegionSize() +
             mBuffer->getUsedAdditionalBufferSize();
     if (!headerPolicy->writeHeaderToBuffer(&headerBuffer, false /* updatesLastUpdatedTime */,
-            unigramCount, bigramCount, extendedRegionSize)) {
+            false /* updatesLastDecayedTime */, unigramCount, bigramCount, extendedRegionSize)) {
         return;
     }
     DictFileWritingUtils::flushAllHeaderAndBodyToFile(fileName, &headerBuffer, mBuffer);
@@ -165,12 +165,15 @@
             MAX_DICTIONARY_SIZE);
     int unigramCount = 0;
     int bigramCount = 0;
-    if (!runGC(rootPtNodeArrayPos, &newDictBuffer, &unigramCount, &bigramCount)) {
+    if (mNeedsToDecay) {
+        ForgettingCurveUtils::sTimeKeeper.setCurrentTime();
+    }
+    if (!runGC(rootPtNodeArrayPos, headerPolicy, &newDictBuffer, &unigramCount, &bigramCount)) {
         return;
     }
     BufferWithExtendableBuffer headerBuffer(0 /* originalBuffer */, 0 /* originalBufferSize */);
     if (!headerPolicy->writeHeaderToBuffer(&headerBuffer, true /* updatesLastUpdatedTime */,
-            unigramCount, bigramCount, 0 /* extendedRegionSize */)) {
+            mNeedsToDecay, unigramCount, bigramCount, 0 /* extendedRegionSize */)) {
         return;
     }
     DictFileWritingUtils::flushAllHeaderAndBodyToFile(fileName, &headerBuffer, &newDictBuffer);
@@ -237,7 +240,8 @@
             int parentOffsetFieldPos = nodeReader->getHeadPos()
                     + DynamicPatriciaTrieWritingUtils::NODE_FLAG_FIELD_SIZE;
             if (!DynamicPatriciaTrieWritingUtils::writeParentPosOffsetAndAdvancePosition(
-                    mBuffer, movedPos, nodeReader->getHeadPos(), &parentOffsetFieldPos)) {
+                    mBuffer, bigramLinkedNodePos, nodeReader->getHeadPos(),
+                    &parentOffsetFieldPos)) {
                 // Parent offset cannot be written because of a bug or a broken dictionary; thus,
                 // we give up to update dictionary.
                 return false;
@@ -481,20 +485,20 @@
 }
 
 bool DynamicPatriciaTrieWritingHelper::runGC(const int rootPtNodeArrayPos,
-        BufferWithExtendableBuffer *const bufferToWrite, int *const outUnigramCount,
-        int *const outBigramCount) {
+        const HeaderPolicy *const headerPolicy, BufferWithExtendableBuffer *const bufferToWrite,
+        int *const outUnigramCount, int *const outBigramCount) {
     DynamicPatriciaTrieReadingHelper readingHelper(mBuffer, mBigramPolicy, mShortcutPolicy);
     readingHelper.initWithPtNodeArrayPos(rootPtNodeArrayPos);
     DynamicPatriciaTrieGcEventListeners
             ::TraversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted
                     traversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted(
-                            this, mBuffer, mIsDecayingDict);
+                            headerPolicy, this, mBuffer, mNeedsToDecay);
     if (!readingHelper.traverseAllPtNodesInPostorderDepthFirstManner(
             &traversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted)) {
         return false;
     }
-    if (mIsDecayingDict && traversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted
-            .getValidUnigramCount() > DecayingUtils::MAX_UNIGRAM_COUNT_AFTER_GC) {
+    if (mNeedsToDecay && traversePolicyToUpdateUnigramProbabilityAndMarkUselessPtNodesAsDeleted
+            .getValidUnigramCount() > ForgettingCurveUtils::MAX_UNIGRAM_COUNT_AFTER_GC) {
         // TODO: Remove more unigrams.
     }
 
@@ -505,9 +509,8 @@
             &traversePolicyToUpdateBigramProbability)) {
         return false;
     }
-
-    if (mIsDecayingDict && traversePolicyToUpdateBigramProbability.getValidBigramEntryCount()
-            > DecayingUtils::MAX_BIGRAM_COUNT_AFTER_GC) {
+    if (mNeedsToDecay && traversePolicyToUpdateBigramProbability.getValidBigramEntryCount()
+            > ForgettingCurveUtils::MAX_BIGRAM_COUNT_AFTER_GC) {
         // TODO: Remove more bigrams.
     }
 
@@ -524,8 +527,8 @@
 
     // Create policy instance for the GCed dictionary.
     DynamicShortcutListPolicy newDictShortcutPolicy(bufferToWrite);
-    DynamicBigramListPolicy newDictBigramPolicy(bufferToWrite, &newDictShortcutPolicy,
-            mIsDecayingDict);
+    DynamicBigramListPolicy newDictBigramPolicy(headerPolicy, bufferToWrite, &newDictShortcutPolicy,
+            mNeedsToDecay);
     // Create reading helper for the GCed dictionary.
     DynamicPatriciaTrieReadingHelper newDictReadingHelper(bufferToWrite, &newDictBigramPolicy,
             &newDictShortcutPolicy);
@@ -544,8 +547,9 @@
 
 int DynamicPatriciaTrieWritingHelper::getUpdatedProbability(const int originalProbability,
         const int newProbability) {
-    if (mIsDecayingDict) {
-        return DecayingUtils::getUpdatedUnigramProbability(originalProbability, newProbability);
+    if (mNeedsToDecay) {
+        return ForgettingCurveUtils::getUpdatedEncodedProbability(originalProbability,
+                newProbability);
     } else {
         return newProbability;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
index ecee2cd..ca86647 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/dynamic_patricia_trie_writing_helper.h
@@ -51,9 +51,9 @@
 
     DynamicPatriciaTrieWritingHelper(BufferWithExtendableBuffer *const buffer,
             DynamicBigramListPolicy *const bigramPolicy,
-            DynamicShortcutListPolicy *const shortcutPolicy, const bool isDecayingDict)
+            DynamicShortcutListPolicy *const shortcutPolicy, const bool needsToDecay)
             : mBuffer(buffer), mBigramPolicy(bigramPolicy), mShortcutPolicy(shortcutPolicy),
-              mIsDecayingDict(isDecayingDict) {}
+              mNeedsToDecay(needsToDecay) {}
 
     ~DynamicPatriciaTrieWritingHelper() {}
 
@@ -94,7 +94,7 @@
     BufferWithExtendableBuffer *const mBuffer;
     DynamicBigramListPolicy *const mBigramPolicy;
     DynamicShortcutListPolicy *const mShortcutPolicy;
-    const bool mIsDecayingDict;
+    const bool mNeedsToDecay;
 
     bool markNodeAsMovedAndSetPosition(const DynamicPatriciaTrieNodeReader *const nodeToUpdate,
             const int movedPos, const int bigramLinkedNodePos);
@@ -128,8 +128,9 @@
             const int probabilityOfNewPtNode, const int *const newNodeCodePoints,
             const int newNodeCodePointCount);
 
-    bool runGC(const int rootPtNodeArrayPos, BufferWithExtendableBuffer *const bufferToWrite,
-            int *const outUnigramCount, int *const outBigramCount);
+    bool runGC(const int rootPtNodeArrayPos, const HeaderPolicy *const headerPolicy,
+            BufferWithExtendableBuffer *const bufferToWrite, int *const outUnigramCount,
+            int *const outBigramCount);
 
     int getUpdatedProbability(const int originalProbability, const int newProbability);
 };
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 9ce9994..eb072fb 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.cpp
@@ -23,6 +23,7 @@
 // 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::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";
 const char *const HeaderPolicy::EXTENDED_REGION_SIZE_KEY = "EXTENDED_REGION_SIZE";
@@ -63,8 +64,8 @@
 }
 
 bool HeaderPolicy::writeHeaderToBuffer(BufferWithExtendableBuffer *const bufferToWrite,
-        const bool updatesLastUpdatedTime, const int unigramCount, const int bigramCount,
-        const int extendedRegionSize) const {
+        const bool updatesLastUpdatedTime, const bool updatesLastDecayedTime,
+        const int unigramCount, const int bigramCount, const int extendedRegionSize) const {
     int writingPos = 0;
     if (!HeaderReadWriteUtils::writeDictionaryVersion(bufferToWrite, mDictFormatVersion,
             &writingPos)) {
@@ -90,6 +91,11 @@
         HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, LAST_UPDATED_TIME_KEY,
                 time(0));
     }
+    if (updatesLastDecayedTime) {
+        // Set current time as a last updated time.
+        HeaderReadWriteUtils::setIntAttribute(&attributeMapTowrite, LAST_DECAYED_TIME_KEY,
+                time(0));
+    }
     if (!HeaderReadWriteUtils::writeHeaderAttributes(bufferToWrite, &attributeMapTowrite,
             &writingPos)) {
         return false;
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 4261667..a9c7805 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
@@ -40,6 +40,8 @@
                       IS_DECAYING_DICT_KEY, false /* defaultValue */)),
               mLastUpdatedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
                       LAST_UPDATED_TIME_KEY, time(0) /* defaultValue */)),
+              mLastDecayedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
+                      LAST_DECAYED_TIME_KEY, time(0) /* defaultValue */)),
               mUnigramCount(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
                       UNIGRAM_COUNT_KEY, 0 /* defaultValue */)),
               mBigramCount(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
@@ -58,6 +60,8 @@
                       IS_DECAYING_DICT_KEY, false /* defaultValue */)),
               mLastUpdatedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
                       LAST_UPDATED_TIME_KEY, time(0) /* defaultValue */)),
+              mLastDecayedTime(HeaderReadWriteUtils::readIntAttributeValue(&mAttributeMap,
+                      LAST_UPDATED_TIME_KEY, time(0) /* defaultValue */)),
               mUnigramCount(0), mBigramCount(0), mExtendedRegionSize(0) {}
 
     ~HeaderPolicy() {}
@@ -90,6 +94,10 @@
         return mLastUpdatedTime;
     }
 
+    AK_FORCE_INLINE int getLastDecayedTime() const {
+        return mLastDecayedTime;
+    }
+
     AK_FORCE_INLINE int getUnigramCount() const {
         return mUnigramCount;
     }
@@ -106,8 +114,8 @@
             int *outValue, int outValueSize) const;
 
     bool writeHeaderToBuffer(BufferWithExtendableBuffer *const bufferToWrite,
-            const bool updatesLastUpdatedTime, const int unigramCount,
-            const int bigramCount, const int extendedRegionSize) const;
+            const bool updatesLastUpdatedTime, const bool updatesLastDecayedTime,
+            const int unigramCount, const int bigramCount, const int extendedRegionSize) const;
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(HeaderPolicy);
@@ -115,6 +123,7 @@
     static const char *const MULTIPLE_WORDS_DEMOTION_RATE_KEY;
     static const char *const IS_DECAYING_DICT_KEY;
     static const char *const LAST_UPDATED_TIME_KEY;
+    static const char *const LAST_DECAYED_TIME_KEY;
     static const char *const UNIGRAM_COUNT_KEY;
     static const char *const BIGRAM_COUNT_KEY;
     static const char *const EXTENDED_REGION_SIZE_KEY;
@@ -128,6 +137,7 @@
     const float mMultiWordCostMultiplier;
     const bool mIsDecayingDict;
     const int mLastUpdatedTime;
+    const int mLastDecayedTime;
     const int mUnigramCount;
     const int mBigramCount;
     const int mExtendedRegionSize;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
index 8d88c68..0f8662a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/patricia_trie_policy.h
@@ -114,7 +114,7 @@
     }
 
     void getProperty(const char *const query, char *const outResult,
-            const int maxResultLength) const {
+            const int maxResultLength) {
         // getProperty is not supported for this class.
         if (maxResultLength > 0) {
             outResult[0] = '\0';
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.cpp
deleted file mode 100644
index 942a742..0000000
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.cpp
+++ /dev/null
@@ -1,129 +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.
- */
-
-#include "suggest/policyimpl/dictionary/utils/decaying_utils.h"
-
-#include "suggest/policyimpl/dictionary/utils/probability_utils.h"
-
-namespace latinime {
-
-const int DecayingUtils::MAX_UNIGRAM_COUNT = 12000;
-const int DecayingUtils::MAX_UNIGRAM_COUNT_AFTER_GC = 10000;
-const int DecayingUtils::MAX_BIGRAM_COUNT = 12000;
-const int DecayingUtils::MAX_BIGRAM_COUNT_AFTER_GC = 10000;
-
-const int DecayingUtils::MAX_COMPUTED_PROBABILITY = 127;
-const int DecayingUtils::MAX_UNIGRAM_PROBABILITY = 120;
-const int DecayingUtils::MIN_VALID_UNIGRAM_PROBABILITY = 24;
-const int DecayingUtils::UNIGRAM_PROBABILITY_STEP = 8;
-const int DecayingUtils::MAX_BIGRAM_PROBABILITY_DELTA = 15;
-const int DecayingUtils::MIN_VALID_BIGRAM_PROBABILITY_DELTA = 3;
-const int DecayingUtils::BIGRAM_PROBABILITY_DELTA_STEP = 1;
-
-/* static */ int DecayingUtils::getProbability(const int encodedUnigramProbability,
-        const int encodedBigramProbabilityDelta) {
-    if (encodedUnigramProbability == NOT_A_PROBABILITY) {
-        return NOT_A_PROBABILITY;
-    } else if (encodedBigramProbabilityDelta == NOT_A_PROBABILITY) {
-        const int rawProbability = ProbabilityUtils::backoff(decodeUnigramProbability(
-                encodedUnigramProbability));
-        return min(getDecayedProbability(rawProbability), MAX_COMPUTED_PROBABILITY);
-    } else {
-        const int rawProbability = ProbabilityUtils::computeProbabilityForBigram(
-                decodeUnigramProbability(encodedUnigramProbability),
-                decodeBigramProbabilityDelta(encodedBigramProbabilityDelta));
-        return min(getDecayedProbability(rawProbability), MAX_COMPUTED_PROBABILITY);
-    }
-}
-
-/* static */ int DecayingUtils::getUpdatedUnigramProbability(const int originalEncodedProbability,
-        const int newProbability) {
-    if (originalEncodedProbability == NOT_A_PROBABILITY) {
-        // The unigram is not in this dictionary.
-        if (newProbability == NOT_A_PROBABILITY) {
-            // The unigram is not in other dictionaries.
-            return 0;
-        } else {
-            return MIN_VALID_UNIGRAM_PROBABILITY;
-        }
-    } else {
-        if (newProbability != NOT_A_PROBABILITY
-                && originalEncodedProbability < MIN_VALID_UNIGRAM_PROBABILITY) {
-            return MIN_VALID_UNIGRAM_PROBABILITY;
-        }
-        return min(originalEncodedProbability + UNIGRAM_PROBABILITY_STEP, MAX_UNIGRAM_PROBABILITY);
-    }
-}
-
-/* static */ int DecayingUtils::getUnigramProbabilityToSave(const int encodedProbability) {
-    return max(encodedProbability - UNIGRAM_PROBABILITY_STEP, 0);
-}
-
-/* static */ int DecayingUtils::getBigramProbabilityDeltaToSave(const int encodedProbabilityDelta) {
-    return max(encodedProbabilityDelta - BIGRAM_PROBABILITY_DELTA_STEP, 0);
-}
-
-/* static */ int DecayingUtils::getUpdatedBigramProbabilityDelta(
-        const int originalEncodedProbabilityDelta, const int newProbability) {
-    if (originalEncodedProbabilityDelta == NOT_A_PROBABILITY) {
-        // The bigram relation is not in this dictionary.
-        if (newProbability == NOT_A_PROBABILITY) {
-            // The bigram target is not in other dictionaries.
-            return 0;
-        } else {
-            return MIN_VALID_BIGRAM_PROBABILITY_DELTA;
-        }
-    } else {
-        if (newProbability != NOT_A_PROBABILITY
-                && originalEncodedProbabilityDelta < MIN_VALID_BIGRAM_PROBABILITY_DELTA) {
-            return MIN_VALID_BIGRAM_PROBABILITY_DELTA;
-        }
-        return min(originalEncodedProbabilityDelta + BIGRAM_PROBABILITY_DELTA_STEP,
-                MAX_BIGRAM_PROBABILITY_DELTA);
-    }
-}
-
-/* static */ int DecayingUtils::isValidUnigram(const int encodedUnigramProbability) {
-    return encodedUnigramProbability >= MIN_VALID_UNIGRAM_PROBABILITY;
-}
-
-/* static */ int DecayingUtils::isValidBigram(const int encodedBigramProbabilityDelta) {
-    return encodedBigramProbabilityDelta >= MIN_VALID_BIGRAM_PROBABILITY_DELTA;
-}
-
-/* static */ int DecayingUtils::decodeUnigramProbability(const int encodedProbability) {
-    const int probability = encodedProbability - MIN_VALID_UNIGRAM_PROBABILITY;
-    if (probability < 0) {
-        return NOT_A_PROBABILITY;
-    } else {
-        return min(probability, MAX_UNIGRAM_PROBABILITY);
-    }
-}
-
-/* static */ int DecayingUtils::decodeBigramProbabilityDelta(const int encodedProbabilityDelta) {
-    const int probabilityDelta = encodedProbabilityDelta - MIN_VALID_BIGRAM_PROBABILITY_DELTA;
-    if (probabilityDelta < 0) {
-        return NOT_A_PROBABILITY;
-    } else {
-        return min(probabilityDelta, MAX_BIGRAM_PROBABILITY_DELTA);
-    }
-}
-
-/* static */ int DecayingUtils::getDecayedProbability(const int rawProbability) {
-    return rawProbability;
-}
-
-} // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.h
deleted file mode 100644
index 1ca0391..0000000
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/decaying_utils.h
+++ /dev/null
@@ -1,70 +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_DECAYING_UTILS_H
-#define LATINIME_DECAYING_UTILS_H
-
-#include "defines.h"
-
-namespace latinime {
-
-// TODO: Check the elapsed time and decrease the probability depending on the time. Time field is
-// required to introduced to each terminal PtNode and bigram entry.
-// TODO: Quit using bigram probability to indicate the delta.
-// TODO: Quit using bigram probability delta.
-class DecayingUtils {
- public:
-    static const int MAX_UNIGRAM_COUNT;
-    static const int MAX_UNIGRAM_COUNT_AFTER_GC;
-    static const int MAX_BIGRAM_COUNT;
-    static const int MAX_BIGRAM_COUNT_AFTER_GC;
-
-    static int getProbability(const int encodedUnigramProbability,
-            const int encodedBigramProbabilityDelta);
-
-    static int getUpdatedUnigramProbability(const int originalEncodedProbability,
-            const int newProbability);
-
-    static int getUpdatedBigramProbabilityDelta(const int originalEncodedProbabilityDelta,
-            const int newProbability);
-
-    static int isValidUnigram(const int encodedUnigramProbability);
-
-    static int isValidBigram(const int encodedProbabilityDelta);
-
-    static int getUnigramProbabilityToSave(const int encodedProbability);
-
-    static int getBigramProbabilityDeltaToSave(const int encodedProbabilityDelta);
-
- private:
-    DISALLOW_IMPLICIT_CONSTRUCTORS(DecayingUtils);
-
-    static const int MAX_COMPUTED_PROBABILITY;
-    static const int MAX_UNIGRAM_PROBABILITY;
-    static const int MIN_VALID_UNIGRAM_PROBABILITY;
-    static const int UNIGRAM_PROBABILITY_STEP;
-    static const int MAX_BIGRAM_PROBABILITY_DELTA;
-    static const int MIN_VALID_BIGRAM_PROBABILITY_DELTA;
-    static const int BIGRAM_PROBABILITY_DELTA_STEP;
-
-    static int decodeUnigramProbability(const int encodedProbability);
-
-    static int decodeBigramProbabilityDelta(const int encodedProbability);
-
-    static int getDecayedProbability(const int rawProbability);
-};
-} // namespace latinime
-#endif /* LATINIME_DECAYING_UTILS_H */
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 f22e94c..994826f 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
@@ -44,7 +44,8 @@
     BufferWithExtendableBuffer headerBuffer(0 /* originalBuffer */, 0 /* originalBufferSize */);
     HeaderPolicy headerPolicy(FormatUtils::VERSION_3, attributeMap);
     headerPolicy.writeHeaderToBuffer(&headerBuffer, true /* updatesLastUpdatedTime */,
-            0 /* unigramCount */, 0 /* bigramCount */, 0 /* extendedRegionSize */);
+            true /* updatesLastDecayedTime */, 0 /* unigramCount */, 0 /* bigramCount */,
+            0 /* extendedRegionSize */);
     BufferWithExtendableBuffer bodyBuffer(0 /* originalBuffer */, 0 /* originalBufferSize */);
     if (!DynamicPatriciaTrieWritingUtils::writeEmptyDictionary(&bodyBuffer, 0 /* rootPos */)) {
         return false;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.cpp
new file mode 100644
index 0000000..1632fd0
--- /dev/null
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.cpp
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+
+#include <cmath>
+#include <ctime>
+#include <stdlib.h>
+
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
+
+#include "suggest/core/policy/dictionary_header_structure_policy.h"
+#include "suggest/policyimpl/dictionary/utils/probability_utils.h"
+
+namespace latinime {
+
+const int ForgettingCurveUtils::MAX_UNIGRAM_COUNT = 12000;
+const int ForgettingCurveUtils::MAX_UNIGRAM_COUNT_AFTER_GC = 10000;
+const int ForgettingCurveUtils::MAX_BIGRAM_COUNT = 12000;
+const int ForgettingCurveUtils::MAX_BIGRAM_COUNT_AFTER_GC = 10000;
+
+const int ForgettingCurveUtils::MAX_COMPUTED_PROBABILITY = 127;
+const int ForgettingCurveUtils::MAX_ENCODED_PROBABILITY = 15;
+const int ForgettingCurveUtils::MIN_VALID_ENCODED_PROBABILITY = 3;
+const int ForgettingCurveUtils::ENCODED_PROBABILITY_STEP = 1;
+// Currently, we try to decay each uni/bigram once every 2 hours. Accordingly, the expected
+// duration of the decay is approximately 66hours.
+const float ForgettingCurveUtils::MIN_PROBABILITY_TO_DECAY = 0.03f;
+const int ForgettingCurveUtils::DECAY_INTERVAL_SECONDS = 2 * 60 * 60;
+
+const ForgettingCurveUtils::ProbabilityTable ForgettingCurveUtils::sProbabilityTable;
+ForgettingCurveUtils::TimeKeeper ForgettingCurveUtils::sTimeKeeper;
+
+void ForgettingCurveUtils::TimeKeeper::setCurrentTime() {
+    mCurrentTime = time(0);
+}
+
+/* static */ int ForgettingCurveUtils::getProbability(const int encodedUnigramProbability,
+        const int encodedBigramProbability) {
+    if (encodedUnigramProbability == NOT_A_PROBABILITY) {
+        return NOT_A_PROBABILITY;
+    } else if (encodedBigramProbability == NOT_A_PROBABILITY) {
+        return backoff(decodeProbability(encodedUnigramProbability));
+    } else {
+        const int unigramProbability = decodeProbability(encodedUnigramProbability);
+        const int bigramProbability = decodeProbability(encodedBigramProbability);
+        return min(max(unigramProbability, bigramProbability), MAX_COMPUTED_PROBABILITY);
+    }
+}
+
+// Caveat: Unlike getProbability(), this method doesn't assume special bigram probability encoding
+// (i.e. unigram probability + bigram probability delta).
+/* static */ int ForgettingCurveUtils::getUpdatedEncodedProbability(
+        const int originalEncodedProbability, const int newProbability) {
+    if (originalEncodedProbability == NOT_A_PROBABILITY) {
+        // The bigram relation is not in this dictionary.
+        if (newProbability == NOT_A_PROBABILITY) {
+            // The bigram target is not in other dictionaries.
+            return 0;
+        } else {
+            return MIN_VALID_ENCODED_PROBABILITY;
+        }
+    } else {
+        if (newProbability != NOT_A_PROBABILITY
+                && originalEncodedProbability < MIN_VALID_ENCODED_PROBABILITY) {
+            return MIN_VALID_ENCODED_PROBABILITY;
+        }
+        return min(originalEncodedProbability + ENCODED_PROBABILITY_STEP, MAX_ENCODED_PROBABILITY);
+    }
+}
+
+/* static */ int ForgettingCurveUtils::isValidEncodedProbability(const int encodedProbability) {
+    return encodedProbability >= MIN_VALID_ENCODED_PROBABILITY;
+}
+
+/* static */ int ForgettingCurveUtils::getEncodedProbabilityToSave(const int encodedProbability,
+        const DictionaryHeaderStructurePolicy *const headerPolicy) {
+    const int elapsedTime = sTimeKeeper.peekCurrentTime() - headerPolicy->getLastDecayedTime();
+    const int decayIterationCount = max(elapsedTime / DECAY_INTERVAL_SECONDS, 1);
+    int currentEncodedProbability = max(min(encodedProbability, MAX_ENCODED_PROBABILITY), 0);
+    // TODO: Implement the decay in more proper way.
+    for (int i = 0; i < decayIterationCount; ++i) {
+        const float currentRate = static_cast<float>(currentEncodedProbability)
+                / static_cast<float>(MAX_ENCODED_PROBABILITY);
+        const float thresholdToDecay = (1.0f - MIN_PROBABILITY_TO_DECAY) * currentRate;
+        const float randValue = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
+        if (thresholdToDecay < randValue) {
+            currentEncodedProbability = max(currentEncodedProbability - ENCODED_PROBABILITY_STEP,
+                    0);
+        }
+    }
+    return currentEncodedProbability;
+}
+
+/* static */ bool ForgettingCurveUtils::needsToDecay(const bool mindsBlockByDecay,
+        const int unigramCount, const int bigramCount,
+        const DictionaryHeaderStructurePolicy *const headerPolicy) {
+    if (unigramCount >= ForgettingCurveUtils::MAX_UNIGRAM_COUNT) {
+        // Unigram count exceeds the limit.
+        return true;
+    } else if (bigramCount >= ForgettingCurveUtils::MAX_BIGRAM_COUNT) {
+        // Bigram count exceeds the limit.
+        return true;
+    }
+    if (mindsBlockByDecay) {
+        return false;
+    }
+    if (headerPolicy->getLastDecayedTime() + DECAY_INTERVAL_SECONDS < time(0)) {
+        // Time to decay.
+        return true;
+    }
+    return false;
+}
+
+/* static */ int ForgettingCurveUtils::decodeProbability(const int encodedProbability) {
+    if (encodedProbability < MIN_VALID_ENCODED_PROBABILITY) {
+        return NOT_A_PROBABILITY;
+    } else {
+        return min(sProbabilityTable.getProbability(encodedProbability), MAX_ENCODED_PROBABILITY);
+    }
+}
+
+// See comments in ProbabilityUtils::backoff().
+/* static */ int ForgettingCurveUtils::backoff(const int unigramProbability) {
+    if (unigramProbability == NOT_A_PROBABILITY) {
+        return NOT_A_PROBABILITY;
+    } else {
+        return max(unigramProbability - 8, 0);
+    }
+}
+
+ForgettingCurveUtils::ProbabilityTable::ProbabilityTable() : mTable() {
+    // Table entry is as follows:
+    // 1, 1, 1, 2, 3, 5, 6, 9, 13, 18, 25, 34, 48, 66, 91, 127.
+    // Note that first MIN_VALID_ENCODED_PROBABILITY values are not used.
+    mTable.resize(MAX_ENCODED_PROBABILITY + 1);
+    for (int i = 0; i <= MAX_ENCODED_PROBABILITY; ++i) {
+        const int probability = static_cast<int>(powf(static_cast<float>(MAX_COMPUTED_PROBABILITY),
+                static_cast<float>(i) / static_cast<float>(MAX_ENCODED_PROBABILITY)));
+         mTable[i] = min(MAX_COMPUTED_PROBABILITY, max(0, probability));
+    }
+}
+
+} // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h
new file mode 100644
index 0000000..2ad4238
--- /dev/null
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h
@@ -0,0 +1,100 @@
+/*
+ * 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_FORGETTING_CURVE_UTILS_H
+#define LATINIME_FORGETTING_CURVE_UTILS_H
+
+#include <vector>
+
+#include "defines.h"
+
+namespace latinime {
+
+class DictionaryHeaderStructurePolicy;
+
+// TODO: Check the elapsed time and decrease the probability depending on the time. Time field is
+// required to introduced to each terminal PtNode and bigram entry.
+// TODO: Quit using bigram probability to indicate the delta.
+class ForgettingCurveUtils {
+ public:
+    class TimeKeeper {
+     public:
+        TimeKeeper() : mCurrentTime(0) {}
+        void setCurrentTime();
+        int peekCurrentTime() const { return mCurrentTime; };
+
+     private:
+        DISALLOW_COPY_AND_ASSIGN(TimeKeeper);
+
+        int mCurrentTime;
+    };
+
+    static const int MAX_UNIGRAM_COUNT;
+    static const int MAX_UNIGRAM_COUNT_AFTER_GC;
+    static const int MAX_BIGRAM_COUNT;
+    static const int MAX_BIGRAM_COUNT_AFTER_GC;
+
+    static TimeKeeper sTimeKeeper;
+
+    static int getProbability(const int encodedUnigramProbability,
+            const int encodedBigramProbability);
+
+    static int getUpdatedEncodedProbability(const int originalEncodedProbability,
+            const int newProbability);
+
+    static int isValidEncodedProbability(const int encodedProbability);
+
+    static int getEncodedProbabilityToSave(const int encodedProbability,
+            const DictionaryHeaderStructurePolicy *const headerPolicy);
+
+    static bool needsToDecay(const bool mindsBlockByDecay, const int unigramCount,
+            const int bigramCount, const DictionaryHeaderStructurePolicy *const headerPolicy);
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(ForgettingCurveUtils);
+
+    class ProbabilityTable {
+     public:
+        ProbabilityTable();
+
+        int getProbability(const int encodedProbability) const {
+            if (encodedProbability < 0 || encodedProbability > static_cast<int>(mTable.size())) {
+                return NOT_A_PROBABILITY;
+            }
+            return mTable[encodedProbability];
+        }
+
+     private:
+        DISALLOW_COPY_AND_ASSIGN(ProbabilityTable);
+
+        std::vector<int> mTable;
+    };
+
+    static const int MAX_COMPUTED_PROBABILITY;
+    static const int MAX_ENCODED_PROBABILITY;
+    static const int MIN_VALID_ENCODED_PROBABILITY;
+    static const int ENCODED_PROBABILITY_STEP;
+    static const float MIN_PROBABILITY_TO_DECAY;
+    static const int DECAY_INTERVAL_SECONDS;
+
+    static const ProbabilityTable sProbabilityTable;
+
+    static int decodeProbability(const int encodedProbability);
+
+    static int backoff(const int unigramProbability);
+};
+} // namespace latinime
+#endif /* LATINIME_FORGETTING_CURVE_UTILS_H */
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
index ecceb60..104eb2a 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
@@ -27,30 +27,30 @@
 const int ScoringParams::MAX_CACHE_DIC_NODE_SIZE_FOR_SINGLE_POINT = 310;
 const int ScoringParams::THRESHOLD_SHORT_WORD_LENGTH = 4;
 
-const float ScoringParams::DISTANCE_WEIGHT_LENGTH = 0.132f;
-const float ScoringParams::PROXIMITY_COST = 0.095f;
-const float ScoringParams::FIRST_CHAR_PROXIMITY_COST = 0.102f;
-const float ScoringParams::FIRST_PROXIMITY_COST = 0.019f;
-const float ScoringParams::OMISSION_COST = 0.458f;
-const float ScoringParams::OMISSION_COST_SAME_CHAR = 0.491f;
-const float ScoringParams::OMISSION_COST_FIRST_CHAR = 0.582f;
-const float ScoringParams::INSERTION_COST = 0.730f;
-const float ScoringParams::TERMINAL_INSERTION_COST = 0.93f;
-const float ScoringParams::INSERTION_COST_SAME_CHAR = 0.586f;
-const float ScoringParams::INSERTION_COST_PROXIMITY_CHAR = 0.70f;
-const float ScoringParams::INSERTION_COST_FIRST_CHAR = 0.623f;
-const float ScoringParams::TRANSPOSITION_COST = 0.526f;
-const float ScoringParams::SPACE_SUBSTITUTION_COST = 0.319f;
-const float ScoringParams::ADDITIONAL_PROXIMITY_COST = 0.380f;
-const float ScoringParams::SUBSTITUTION_COST = 0.383f;
-const float ScoringParams::COST_NEW_WORD = 0.042f;
-const float ScoringParams::COST_SECOND_OR_LATER_WORD_FIRST_CHAR_UPPERCASE = 0.25f;
-const float ScoringParams::DISTANCE_WEIGHT_LANGUAGE = 1.123f;
-const float ScoringParams::COST_FIRST_LOOKAHEAD = 0.545f;
-const float ScoringParams::COST_LOOKAHEAD = 0.073f;
-const float ScoringParams::HAS_PROXIMITY_TERMINAL_COST = 0.093f;
-const float ScoringParams::HAS_EDIT_CORRECTION_TERMINAL_COST = 0.041f;
-const float ScoringParams::HAS_MULTI_WORD_TERMINAL_COST = 0.447f;
+const float ScoringParams::DISTANCE_WEIGHT_LENGTH = 0.1524f;
+const float ScoringParams::PROXIMITY_COST = 0.0694f;
+const float ScoringParams::FIRST_CHAR_PROXIMITY_COST = 0.072f;
+const float ScoringParams::FIRST_PROXIMITY_COST = 0.07788f;
+const float ScoringParams::OMISSION_COST = 0.4676f;
+const float ScoringParams::OMISSION_COST_SAME_CHAR = 0.399f;
+const float ScoringParams::OMISSION_COST_FIRST_CHAR = 0.5256f;
+const float ScoringParams::INSERTION_COST = 0.7248f;
+const float ScoringParams::TERMINAL_INSERTION_COST = 0.8128f;
+const float ScoringParams::INSERTION_COST_SAME_CHAR = 0.5508f;
+const float ScoringParams::INSERTION_COST_PROXIMITY_CHAR = 0.674f;
+const float ScoringParams::INSERTION_COST_FIRST_CHAR = 0.639f;
+const float ScoringParams::TRANSPOSITION_COST = 0.5608f;
+const float ScoringParams::SPACE_SUBSTITUTION_COST = 0.339f;
+const float ScoringParams::ADDITIONAL_PROXIMITY_COST = 0.4576f;
+const float ScoringParams::SUBSTITUTION_COST = 0.3806f;
+const float ScoringParams::COST_NEW_WORD = 0.0312f;
+const float ScoringParams::COST_SECOND_OR_LATER_WORD_FIRST_CHAR_UPPERCASE = 0.3224f;
+const float ScoringParams::DISTANCE_WEIGHT_LANGUAGE = 1.1214f;
+const float ScoringParams::COST_FIRST_LOOKAHEAD = 0.4836f;
+const float ScoringParams::COST_LOOKAHEAD = 0.00624f;
+const float ScoringParams::HAS_PROXIMITY_TERMINAL_COST = 0.06836f;
+const float ScoringParams::HAS_EDIT_CORRECTION_TERMINAL_COST = 0.0362f;
+const float ScoringParams::HAS_MULTI_WORD_TERMINAL_COST = 0.4182f;
 const float ScoringParams::TYPING_BASE_OUTPUT_SCORE = 1.0f;
 const float ScoringParams::TYPING_MAX_OUTPUT_SCORE_PER_INPUT = 0.1f;
 const float ScoringParams::NORMALIZED_SPATIAL_DISTANCE_THRESHOLD_FOR_EDIT = 0.045f;
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
index 89e53f4..007c19e 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_traversal.h
@@ -101,7 +101,7 @@
         }
         const int16_t pointIndex = dicNode->getInputIndex(0);
         return pointIndex <= inputSize && !dicNode->isTotalInputSizeExceedingLimit()
-                && !dicNode->shouldBeFilterdBySafetyNetForBigram();
+                && !dicNode->shouldBeFilteredBySafetyNetForBigram();
     }
 
     AK_FORCE_INLINE bool shouldDepthLevelCache(
diff --git a/native/jni/src/utils/char_utils.h b/native/jni/src/utils/char_utils.h
index 2e735a8..41663c8 100644
--- a/native/jni/src/utils/char_utils.h
+++ b/native/jni/src/utils/char_utils.h
@@ -75,6 +75,16 @@
         return c;
     }
 
+    static AK_FORCE_INLINE int getSpaceCount(const int *const codePointBuffer, const int length) {
+        int spaceCount = 0;
+        for (int i = 0; i < length; ++i) {
+            if (codePointBuffer[i] == KEYCODE_SPACE) {
+                ++spaceCount;
+            }
+        }
+        return spaceCount;
+    }
+
     static unsigned short latin_tolower(const unsigned short c);
 
  private:
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index cf85d97..cd5384e 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -18,20 +18,29 @@
 
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Pair;
 
+import com.android.inputmethod.latin.makedict.CodePointUtils;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Random;
 
 @LargeTest
 public class BinaryDictionaryDecayingTests extends AndroidTestCase {
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
     private static final String TEST_LOCALE = "test";
 
+    // Note that these are corresponding definitions in native code in
+    // latinime::DynamicPatriciaTriePolicy.
+    private static final String SET_NEEDS_TO_DECAY_FOR_TESTING_KEY =
+            "SET_NEEDS_TO_DECAY_FOR_TESTING";
+
     private static final int DUMMY_PROBABILITY = 0;
 
     @Override
@@ -45,14 +54,20 @@
     }
 
     private void forcePassingShortTime(final BinaryDictionary binaryDictionary) {
-        binaryDictionary.flushWithGC();
+        // Entries having low probability would be suppressed once in 3 GCs.
+        final int count = 3;
+        for (int i = 0; i < count; i++) {
+            binaryDictionary.getPropertyForTests(SET_NEEDS_TO_DECAY_FOR_TESTING_KEY);
+            binaryDictionary.flushWithGC();
+        }
     }
 
     private void forcePassingLongTime(final BinaryDictionary binaryDictionary) {
         // Currently, probabilities are decayed when GC is run. All entries that have never been
-        // typed in 32 GCs are removed.
-        final int count = 32;
+        // typed in 128 GCs would be removed.
+        final int count = 128;
         for (int i = 0; i < count; i++) {
+            binaryDictionary.getPropertyForTests(SET_NEEDS_TO_DECAY_FOR_TESTING_KEY);
             binaryDictionary.flushWithGC();
         }
     }
@@ -110,11 +125,16 @@
         binaryDictionary.addBigramWords("a", "c", DUMMY_PROBABILITY);
         assertTrue(binaryDictionary.isValidBigram("a", "c"));
 
+        // Add bigrams of not valid unigrams.
+        binaryDictionary.addBigramWords("x", "y", Dictionary.NOT_A_PROBABILITY);
+        assertFalse(binaryDictionary.isValidBigram("x", "y"));
+        binaryDictionary.addBigramWords("x", "y", DUMMY_PROBABILITY);
+        assertFalse(binaryDictionary.isValidBigram("x", "y"));
+
         binaryDictionary.close();
         dictFile.delete();
     }
 
-    // TODO: Add large tests.
     public void testDecayingProbability() {
         File dictFile = null;
         try {
@@ -168,4 +188,121 @@
         binaryDictionary.close();
         dictFile.delete();
     }
+
+    public void testAddManyUnigramsToDecayingDict() {
+        final int unigramCount = 30000;
+        final int unigramTypedCount = 100000;
+        final int codePointSetSize = 50;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
+
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } 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 */);
+
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
+        final ArrayList<String> words = new ArrayList<String>();
+
+        for (int i = 0; i < unigramCount; i++) {
+            final String word = CodePointUtils.generateWord(random, codePointSet);
+            words.add(word);
+        }
+
+        final int maxUnigramCount = Integer.parseInt(
+                binaryDictionary.getPropertyForTests(BinaryDictionary.MAX_UNIGRAM_COUNT_QUERY));
+        for (int i = 0; i < unigramTypedCount; i++) {
+            final String word = words.get(random.nextInt(words.size()));
+            binaryDictionary.addUnigramWord(word, DUMMY_PROBABILITY);
+
+            if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
+                final int unigramCountBeforeGC =
+                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                                BinaryDictionary.UNIGRAM_COUNT_QUERY));
+                while (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
+                    binaryDictionary.flushWithGC();
+                }
+                final int unigramCountAfterGC =
+                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                                BinaryDictionary.UNIGRAM_COUNT_QUERY));
+                assertTrue(unigramCountBeforeGC > unigramCountAfterGC);
+            }
+        }
+
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+                BinaryDictionary.UNIGRAM_COUNT_QUERY)) > 0);
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+                BinaryDictionary.UNIGRAM_COUNT_QUERY)) <= maxUnigramCount);
+    }
+
+    public void testAddManyBigramsToDecayingDict() {
+        final int unigramCount = 5000;
+        final int bigramCount = 30000;
+        final int bigramTypedCount = 100000;
+        final int codePointSetSize = 50;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
+
+        File dictFile = null;
+        try {
+            dictFile = createEmptyDictionaryAndGetFile("TestBinaryDictionary");
+        } 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 */);
+
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
+        final ArrayList<String> words = new ArrayList<String>();
+        final ArrayList<Pair<String, String>> bigrams = new ArrayList<Pair<String, String>>();
+
+        for (int i = 0; i < unigramCount; ++i) {
+            final String word = CodePointUtils.generateWord(random, codePointSet);
+            words.add(word);
+        }
+        for (int i = 0; i < bigramCount; ++i) {
+            final int word0Index = random.nextInt(words.size());
+            int word1Index = random.nextInt(words.size() - 1);
+            if (word1Index >= word0Index) {
+                word1Index += 1;
+            }
+            final String word0 = words.get(word0Index);
+            final String word1 = words.get(word1Index);
+            final Pair<String, String> bigram = new Pair<String, String>(word0, word1);
+            bigrams.add(bigram);
+        }
+
+        final int maxBigramCount = Integer.parseInt(
+                binaryDictionary.getPropertyForTests(BinaryDictionary.MAX_BIGRAM_COUNT_QUERY));
+        for (int i = 0; i < bigramTypedCount; ++i) {
+            final Pair<String, String> bigram = bigrams.get(random.nextInt(bigrams.size()));
+            binaryDictionary.addUnigramWord(bigram.first, DUMMY_PROBABILITY);
+            binaryDictionary.addUnigramWord(bigram.second, DUMMY_PROBABILITY);
+            binaryDictionary.addBigramWords(bigram.first, bigram.second, DUMMY_PROBABILITY);
+
+            if (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
+                final int bigramCountBeforeGC =
+                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                                BinaryDictionary.BIGRAM_COUNT_QUERY));
+                while (binaryDictionary.needsToRunGC(true /* mindsBlockByGC */)) {
+                    binaryDictionary.flushWithGC();
+                }
+                final int bigramCountAfterGC =
+                        Integer.parseInt(binaryDictionary.getPropertyForTests(
+                                BinaryDictionary.BIGRAM_COUNT_QUERY));
+                assertTrue(bigramCountBeforeGC > bigramCountAfterGC);
+            }
+        }
+
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+                BinaryDictionary.BIGRAM_COUNT_QUERY)) > 0);
+        assertTrue(Integer.parseInt(binaryDictionary.getPropertyForTests(
+                BinaryDictionary.BIGRAM_COUNT_QUERY)) <= maxBigramCount);
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index 6a21522..5b8f0e9 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -18,6 +18,7 @@
 
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
+import android.text.TextUtils;
 import android.util.Pair;
 
 import com.android.inputmethod.latin.makedict.CodePointUtils;
@@ -126,7 +127,7 @@
     public void testRandomlyAddUnigramWord() {
         final int wordCount = 1000;
         final int codePointSetSize = 50;
-        final int seed = 123456789;
+        final long seed = System.currentTimeMillis();
 
         File dictFile = null;
         try {
@@ -223,7 +224,8 @@
         final int wordCount = 100;
         final int bigramCount = 1000;
         final int codePointSetSize = 50;
-        final int seed = 11111;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
 
         File dictFile = null;
         try {
@@ -234,43 +236,42 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
         final ArrayList<String> words = new ArrayList<String>();
-        // Test a word that isn't contained within the dictionary.
-        final Random random = new Random(seed);
+        final ArrayList<Pair<String, String>> bigramWords = new ArrayList<Pair<String,String>>();
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
-        final int[] unigramProbabilities = new int[wordCount];
+        final HashMap<String, Integer> unigramProbabilities = new HashMap<String, Integer>();
+        final HashMap<Pair<String, String>, Integer> bigramProbabilities =
+                new HashMap<Pair<String, String>, Integer>();
+
         for (int i = 0; i < wordCount; ++i) {
             final String word = CodePointUtils.generateWord(random, codePointSet);
             words.add(word);
             final int unigramProbability = random.nextInt(0xFF);
-            unigramProbabilities[i] = unigramProbability;
+            unigramProbabilities.put(word, unigramProbability);
             binaryDictionary.addUnigramWord(word, unigramProbability);
         }
 
-        final int[][] probabilities = new int[wordCount][wordCount];
-
-        for (int i = 0; i < wordCount; ++i) {
-            for (int j = 0; j < wordCount; ++j) {
-                probabilities[i][j] = Dictionary.NOT_A_PROBABILITY;
-            }
-        }
-
         for (int i = 0; i < bigramCount; i++) {
-            final int word0Index = random.nextInt(wordCount);
-            final int word1Index = random.nextInt(wordCount);
-            final String word0 = words.get(word0Index);
-            final String word1 = words.get(word1Index);
+            final String word0 = words.get(random.nextInt(wordCount));
+            final String word1 = words.get(random.nextInt(wordCount));
+            if (TextUtils.equals(word0, word1)) {
+                continue;
+            }
+            final Pair<String, String> bigram = new Pair<String, String>(word0, word1);
+            bigramWords.add(bigram);
             final int bigramProbability = random.nextInt(0xF);
-            probabilities[word0Index][word1Index] = binaryDictionary.calculateProbability(
-                    unigramProbabilities[word1Index], bigramProbability);
+            bigramProbabilities.put(bigram, bigramProbability);
             binaryDictionary.addBigramWords(word0, word1, bigramProbability);
         }
 
-        for (int i = 0; i < words.size(); i++) {
-            for (int j = 0; j < words.size(); j++) {
-                assertEquals(probabilities[i][j],
-                        binaryDictionary.getBigramProbability(words.get(i), words.get(j)));
-            }
+        for (final Pair<String, String> bigram : bigramWords) {
+            final int unigramProbability = unigramProbabilities.get(bigram.second);
+            final int bigramProbability = bigramProbabilities.get(bigram);
+            final int probability = binaryDictionary.calculateProbability(unigramProbability,
+                    bigramProbability);
+            assertEquals(probability,
+                    binaryDictionary.getBigramProbability(bigram.first, bigram.second));
         }
 
         dictFile.delete();
@@ -419,8 +420,8 @@
         final int wordCount = 100;
         final int bigramCount = 1000;
         final int codePointSetSize = 30;
-        // TODO: Use various seeds such as a current timestamp to make this test more random.
-        final int seed = 314159265;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
 
         File dictFile = null;
         try {
@@ -432,35 +433,32 @@
         BinaryDictionary binaryDictionary = new BinaryDictionary(dictFile.getAbsolutePath(),
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
+
         final ArrayList<String> words = new ArrayList<String>();
-        // Test a word that isn't contained within the dictionary.
-        final Random random = new Random(seed);
+        final ArrayList<Pair<String, String>> bigramWords = new ArrayList<Pair<String,String>>();
         final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
-        final int[] unigramProbabilities = new int[wordCount];
+        final HashMap<String, Integer> unigramProbabilities = new HashMap<String, Integer>();
+        final HashMap<Pair<String, String>, Integer> bigramProbabilities =
+                new HashMap<Pair<String, String>, Integer>();
+
         for (int i = 0; i < wordCount; ++i) {
             final String word = CodePointUtils.generateWord(random, codePointSet);
             words.add(word);
             final int unigramProbability = random.nextInt(0xFF);
-            unigramProbabilities[i] = unigramProbability;
+            unigramProbabilities.put(word, unigramProbability);
             binaryDictionary.addUnigramWord(word, unigramProbability);
         }
 
-        final int[][] probabilities = new int[wordCount][wordCount];
-
-        for (int i = 0; i < wordCount; ++i) {
-            for (int j = 0; j < wordCount; ++j) {
-                probabilities[i][j] = Dictionary.NOT_A_PROBABILITY;
-            }
-        }
-
         for (int i = 0; i < bigramCount; i++) {
-            final int word0Index = random.nextInt(wordCount);
-            final int word1Index = random.nextInt(wordCount);
-            final String word0 = words.get(word0Index);
-            final String word1 = words.get(word1Index);
+            final String word0 = words.get(random.nextInt(wordCount));
+            final String word1 = words.get(random.nextInt(wordCount));
+            if (TextUtils.equals(word0, word1)) {
+                continue;
+            }
+            final Pair<String, String> bigram = new Pair<String, String>(word0, word1);
+            bigramWords.add(bigram);
             final int bigramProbability = random.nextInt(0xF);
-            probabilities[word0Index][word1Index] = binaryDictionary.calculateProbability(
-                    unigramProbabilities[word1Index], bigramProbability);
+            bigramProbabilities.put(bigram, bigramProbability);
             binaryDictionary.addBigramWords(word0, word1, bigramProbability);
         }
 
@@ -470,12 +468,15 @@
                 0 /* offset */, dictFile.length(), true /* useFullEditDistance */,
                 Locale.getDefault(), TEST_LOCALE, true /* isUpdatable */);
 
-        for (int i = 0; i < words.size(); i++) {
-            for (int j = 0; j < words.size(); j++) {
-                assertEquals(probabilities[i][j],
-                        binaryDictionary.getBigramProbability(words.get(i), words.get(j)));
-            }
+        for (final Pair<String, String> bigram : bigramWords) {
+            final int unigramProbability = unigramProbabilities.get(bigram.second);
+            final int bigramProbability = bigramProbabilities.get(bigram);
+            final int probability = binaryDictionary.calculateProbability(unigramProbability,
+                    bigramProbability);
+            assertEquals(probability,
+                    binaryDictionary.getBigramProbability(bigram.first, bigram.second));
         }
+
         dictFile.delete();
     }
 
@@ -487,8 +488,8 @@
         final float addBigramProb = 0.8f;
         final float removeBigramProb = 0.2f;
         final int codePointSetSize = 30;
-        final int seed = 141421356;
 
+        final long seed = System.currentTimeMillis();
         final Random random = new Random(seed);
 
         File dictFile = null;
@@ -539,6 +540,9 @@
                     }
                     final String word0 = words.get(word0Index);
                     final String word1 = words.get(word1Index);
+                    if (TextUtils.equals(word0, word1)) {
+                        continue;
+                    }
                     final int bigramProbability = random.nextInt(0xF);
                     final Pair<String, String> bigram = new Pair<String, String>(word0, word1);
                     bigramWords.add(bigram);
@@ -586,8 +590,8 @@
     public void testAddManyUnigramsAndFlushWithGC() {
         final int flashWithGCIterationCount = 3;
         final int codePointSetSize = 50;
-        final int seed = 22360679;
 
+        final long seed = System.currentTimeMillis();
         final Random random = new Random(seed);
 
         File dictFile = null;
@@ -632,8 +636,7 @@
         final int codePointSetSize = 50;
         final int unigramCountPerIteration = 1000;
         final int bigramCountPerIteration = 2000;
-        final int seed = 1123581321;
-
+        final long seed = System.currentTimeMillis();
         final Random random = new Random(seed);
 
         File dictFile = null;
@@ -661,6 +664,9 @@
             for (int j = 0; j < bigramCountPerIteration; j++) {
                 final String word0 = words.get(random.nextInt(words.size()));
                 final String word1 = words.get(random.nextInt(words.size()));
+                if (TextUtils.equals(word0, word1)) {
+                    continue;
+                }
                 bigrams.add(new Pair<String, String>(word0, word1));
                 final int bigramProbability = random.nextInt(0xF);
                 binaryDictionary.addBigramWords(word0, word1, bigramProbability);
diff --git a/tests/src/com/android/inputmethod/latin/ExpandableDictionaryTests.java b/tests/src/com/android/inputmethod/latin/ExpandableDictionaryTests.java
index ecf3af7..6aae104 100644
--- a/tests/src/com/android/inputmethod/latin/ExpandableDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/ExpandableDictionaryTests.java
@@ -26,13 +26,16 @@
 public class ExpandableDictionaryTests extends AndroidTestCase {
 
     private final static int UNIGRAM_FREQ = 50;
+    // See UserBinaryDictionary for more information about this variable.
+    // For tests, its actual value does not matter.
+    private final static int SHORTCUT_FREQ = 14;
 
     public void testAddWordAndGetWordFrequency() {
         final ExpandableDictionary dict = new ExpandableDictionary(Dictionary.TYPE_USER);
 
         // Add words
-        dict.addWord("abcde", "abcde", UNIGRAM_FREQ);
-        dict.addWord("abcef", null, UNIGRAM_FREQ + 1);
+        dict.addWord("abcde", "abcde", UNIGRAM_FREQ, SHORTCUT_FREQ);
+        dict.addWord("abcef", null, UNIGRAM_FREQ + 1, 0);
 
         // Check words
         assertFalse(dict.isValidWord("abcde"));
@@ -40,16 +43,16 @@
         assertTrue(dict.isValidWord("abcef"));
         assertEquals(UNIGRAM_FREQ+1, dict.getWordFrequency("abcef"));
 
-        dict.addWord("abc", null, UNIGRAM_FREQ + 2);
+        dict.addWord("abc", null, UNIGRAM_FREQ + 2, 0);
         assertTrue(dict.isValidWord("abc"));
         assertEquals(UNIGRAM_FREQ + 2, dict.getWordFrequency("abc"));
 
         // Add existing word with lower frequency
-        dict.addWord("abc", null, UNIGRAM_FREQ);
+        dict.addWord("abc", null, UNIGRAM_FREQ, 0);
         assertEquals(UNIGRAM_FREQ + 2, dict.getWordFrequency("abc"));
 
         // Add existing word with higher frequency
-        dict.addWord("abc", null, UNIGRAM_FREQ + 3);
+        dict.addWord("abc", null, UNIGRAM_FREQ + 3, 0);
         assertEquals(UNIGRAM_FREQ + 3, dict.getWordFrequency("abc"));
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index cc2569f..6bc8b9d 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -179,10 +179,17 @@
     }
 
     public void testDoubleSpace() {
-        final String STRING_TO_TYPE = "this  ";
-        final String EXPECTED_RESULT = "this. ";
-        type(STRING_TO_TYPE);
-        assertEquals("double space make a period", EXPECTED_RESULT, mEditText.getText().toString());
+        // U+1F607 is an emoji
+        final String[] STRINGS_TO_TYPE =
+                new String[] { "this   ", "a+  ", "\u1F607  ", "..  ", ")  ", "(  ", "%  " };
+        final String[] EXPECTED_RESULTS =
+                new String[] { "this.  ", "a+. ", "\u1F607. ", "..  ", "). ", "(  ", "%  " };
+        for (int i = 0; i < STRINGS_TO_TYPE.length; ++i) {
+            mEditText.setText("");
+            type(STRINGS_TO_TYPE[i]);
+            assertEquals("double space processing", EXPECTED_RESULTS[i],
+                    mEditText.getText().toString());
+        }
     }
 
     public void testCancelDoubleSpace() {
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 234bb1b..b9b52a6 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -238,12 +238,16 @@
     }
 
     protected void changeLanguage(final String locale) {
+        changeLanguageWithoutWait(locale);
+        waitForDictionaryToBeLoaded();
+    }
+
+    protected void changeLanguageWithoutWait(final String locale) {
         mEditText.mCurrentLocale = LocaleUtils.constructLocaleFromString(locale);
         SubtypeSwitcher.getInstance().forceLocale(mEditText.mCurrentLocale);
         mLatinIME.loadKeyboard();
         runMessages();
         mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
-        waitForDictionaryToBeLoaded();
     }
 
     protected void changeKeyboardLocaleAndDictLocale(final String keyboardLocale,
diff --git a/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java b/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java
new file mode 100644
index 0000000..5e98cdf
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/LatinImeStressTests.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.inputmethod.latin.makedict.CodePointUtils;
+
+import java.util.Random;
+
+@LargeTest
+public class LatinImeStressTests extends InputTestsBase {
+    public void testSwitchLanguagesAndInputLatinRandomCodePoints() {
+        final String[] locales = {"en_US", "de", "el", "es", "fi", "it", "nl", "pt", "ru"};
+        final int switchCount = 50;
+        final int maxWordCountToTypeInEachIteration = 20;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
+        final int codePointSetSize = 30;
+        final int[] codePointSet = CodePointUtils.LATIN_ALPHABETS_LOWER;
+        for (int i = 0; i < switchCount; ++i) {
+            changeLanguageWithoutWait(locales[random.nextInt(locales.length)]);
+            final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration);
+            for (int j = 0; j < wordCount; ++j) {
+                final String word = CodePointUtils.generateWord(random, codePointSet);
+                type(word);
+            }
+        }
+    }
+    public void testSwitchLanguagesAndInputRandamCodePoints() {
+        final String[] locales = {"en_US", "de", "el", "es", "fi", "it", "nl", "pt", "ru"};
+        final int switchCount = 50;
+        final int maxWordCountToTypeInEachIteration = 20;
+        final long seed = System.currentTimeMillis();
+        final Random random = new Random(seed);
+        final int codePointSetSize = 30;
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(codePointSetSize, random);
+        for (int i = 0; i < switchCount; ++i) {
+            changeLanguageWithoutWait(locales[random.nextInt(locales.length)]);
+            final int wordCount = random.nextInt(maxWordCountToTypeInEachIteration);
+            for (int j = 0; j < wordCount; ++j) {
+                final String word = CodePointUtils.generateWord(random, codePointSet);
+                type(word);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index a4d9426..0189b33 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -104,7 +104,9 @@
         }
 
         sStarBigrams.put(0, new ArrayList<Integer>());
-        for (int i = 1; i < sWords.size(); ++i) {
+        // MAX - 1 because we added one above already
+        final int maxBigrams = Math.min(sWords.size(), FormatSpec.MAX_BIGRAMS_IN_A_PTNODE - 1);
+        for (int i = 1; i < maxBigrams; ++i) {
             sStarBigrams.get(0).add(i);
         }
 
@@ -544,8 +546,7 @@
     }
 
     private long checkGetTerminalPosition(final DictDecoder dictDecoder, final String word,
-            int index, boolean contained) {
-        final int expectedFrequency = (UNIGRAM_FREQ + index) % 255;
+            final boolean contained) {
         long diff = -1;
         int position = -1;
         try {
@@ -603,7 +604,7 @@
         // Test a word that is contained within the dictionary.
         long sum = 0;
         for (int i = 0; i < sWords.size(); ++i) {
-            final long time = checkGetTerminalPosition(dictDecoder, sWords.get(i), i, true);
+            final long time = checkGetTerminalPosition(dictDecoder, sWords.get(i), true);
             sum += time == -1 ? 0 : time;
         }
         Log.d(TAG, "per search : " + (((double)sum) / sWords.size() / 1000000) + " : " + message
@@ -616,11 +617,11 @@
         for (int i = 0; i < 1000; ++i) {
             final String word = CodePointUtils.generateWord(random, codePointSet);
             if (sWords.indexOf(word) != -1) continue;
-            checkGetTerminalPosition(dictDecoder, word, i, false);
+            checkGetTerminalPosition(dictDecoder, word, false);
         }
     }
 
-    private void runGetTerminalPositionTests(final ArrayList<String> results, final int bufferType,
+    private void runGetTerminalPositionTests(final int bufferType,
             final FormatOptions formatOptions) {
         runGetTerminalPosition(sWords, sEmptyBigrams, bufferType, formatOptions, "unigram");
     }
@@ -628,24 +629,24 @@
     public void testGetTerminalPosition() {
         final ArrayList<String> results = CollectionUtils.newArrayList();
 
-        runGetTerminalPositionTests(results, USE_BYTE_ARRAY, VERSION2);
-        runGetTerminalPositionTests(results, USE_BYTE_ARRAY, VERSION3_WITHOUT_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_ARRAY, VERSION3_WITH_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_ARRAY, VERSION4_WITHOUT_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION2);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION3_WITHOUT_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION3_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION4_WITHOUT_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_ARRAY, VERSION4_WITH_DYNAMIC_UPDATE);
 
-        runGetTerminalPositionTests(results, USE_BYTE_BUFFER, VERSION2);
-        runGetTerminalPositionTests(results, USE_BYTE_BUFFER, VERSION3_WITHOUT_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_BUFFER, VERSION3_WITH_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_BUFFER, VERSION4_WITHOUT_DYNAMIC_UPDATE);
-        runGetTerminalPositionTests(results, USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION2);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION3_WITHOUT_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION3_WITH_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION4_WITHOUT_DYNAMIC_UPDATE);
+        runGetTerminalPositionTests(USE_BYTE_BUFFER, VERSION4_WITH_DYNAMIC_UPDATE);
 
         for (final String result : results) {
             Log.d(TAG, result);
         }
     }
 
-    public void testDeleteWord() {
+    private void runTestDeleteWord(final FormatOptions formatOptions) {
         final String dictName = "testDeleteWord";
         final String dictVersion = Long.toString(System.currentTimeMillis());
         final File file = setUpDictionaryFile(dictName, dictVersion);
@@ -654,31 +655,37 @@
                 new FusionDictionary.DictionaryOptions(
                         new HashMap<String, String>(), false, false));
         addUnigrams(sWords.size(), dict, sWords, null /* shortcutMap */);
-        timeWritingDictToFile(file, dict, VERSION3_WITH_DYNAMIC_UPDATE);
+        timeWritingDictToFile(file, dict, formatOptions);
 
-        final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file, DictDecoder.USE_BYTEARRAY);
-        try {
-            dictDecoder.openDictBuffer();
-        } catch (IOException e) {
-            // ignore
-            Log.e(TAG, "IOException while opening the buffer", e);
+        final DictUpdater dictUpdater;
+        if (formatOptions.mVersion == 3) {
+            dictUpdater = new Ver3DictUpdater(file, DictDecoder.USE_WRITABLE_BYTEBUFFER);
+        } else if (formatOptions.mVersion == 4) {
+            dictUpdater = new Ver4DictUpdater(file, DictDecoder.USE_WRITABLE_BYTEBUFFER);
+        } else {
+            throw new RuntimeException("DictUpdater for version " + formatOptions.mVersion
+                    + " doesn't exist.");
         }
-        assertTrue("Can't get the buffer", dictDecoder.isDictBufferOpen());
 
         try {
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
-                    dictDecoder.getTerminalPosition(sWords.get(0)));
-            DynamicBinaryDictIOUtils.deleteWord(dictDecoder, sWords.get(0));
+                    dictUpdater.getTerminalPosition(sWords.get(0)));
+            dictUpdater.deleteWord(sWords.get(0));
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    dictDecoder.getTerminalPosition(sWords.get(0)));
+                    dictUpdater.getTerminalPosition(sWords.get(0)));
 
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD,
-                    dictDecoder.getTerminalPosition(sWords.get(5)));
-            DynamicBinaryDictIOUtils.deleteWord(dictDecoder, sWords.get(5));
+                    dictUpdater.getTerminalPosition(sWords.get(5)));
+            dictUpdater.deleteWord(sWords.get(5));
             assertEquals(FormatSpec.NOT_VALID_WORD,
-                    dictDecoder.getTerminalPosition(sWords.get(5)));
+                    dictUpdater.getTerminalPosition(sWords.get(5)));
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         }
     }
+
+    public void testDeleteWord() {
+        runTestDeleteWord(VERSION3_WITH_DYNAMIC_UPDATE);
+        runTestDeleteWord(VERSION4_WITH_DYNAMIC_UPDATE);
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
index a837494..afe5adb 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtilsTests.java
@@ -27,9 +27,7 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
-import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -47,6 +45,9 @@
 
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
 
+    private static final int VERSION3 = 3;
+    private static final int VERSION4 = 4;
+
     private static final String[] CHARACTERS = {
         "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
         "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
@@ -185,47 +186,42 @@
     // return amount of time to insert a word
     private long insertAndCheckWord(final File file, final String word, final int frequency,
             final boolean exist, final ArrayList<WeightedString> bigrams,
-            final ArrayList<WeightedString> shortcuts) {
-        BufferedOutputStream outStream = null;
+            final ArrayList<WeightedString> shortcuts, final int formatVersion) {
         long amountOfTime = -1;
         try {
-            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file,
-                    DictDecoder.USE_WRITABLE_BYTEBUFFER);
-            dictDecoder.openDictBuffer();
-            outStream = new BufferedOutputStream(new FileOutputStream(file, true));
+            final DictUpdater dictUpdater;
+            if (formatVersion == VERSION3) {
+                dictUpdater = new Ver3DictUpdater(file, DictDecoder.USE_WRITABLE_BYTEBUFFER);
+            } else {
+                throw new RuntimeException("DictUpdater for version " + formatVersion + " doesn't"
+                        + " exist.");
+            }
 
             if (!exist) {
                 assertEquals(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
             }
             final long now = System.nanoTime();
-            DynamicBinaryDictIOUtils.insertWord(dictDecoder, outStream, word, frequency, bigrams,
-                    shortcuts, false, false);
+            dictUpdater.insertWord(word, frequency, bigrams, shortcuts, false, false);
             amountOfTime = System.nanoTime() - now;
-            outStream.flush();
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
-            outStream.close();
         } catch (IOException e) {
             Log.e(TAG, "Raised an IOException while inserting a word", e);
         } catch (UnsupportedFormatException e) {
             Log.e(TAG, "Raised an UnsupportedFormatException error while inserting a word", e);
-        } finally {
-            if (outStream != null) {
-                try {
-                    outStream.close();
-                } catch (IOException e) {
-                    Log.e(TAG, "Failed to close the output stream", e);
-                }
-            }
         }
         return amountOfTime;
     }
 
-    private void deleteWord(final File file, final String word) {
+    private void deleteWord(final File file, final String word, final int formatVersion) {
         try {
-            final Ver3DictDecoder dictDecoder = new Ver3DictDecoder(file,
-                    DictDecoder.USE_WRITABLE_BYTEBUFFER);
-            dictDecoder.openDictBuffer();
-            DynamicBinaryDictIOUtils.deleteWord(dictDecoder, word);
+            final DictUpdater dictUpdater;
+            if (formatVersion == VERSION3) {
+                dictUpdater = new Ver3DictUpdater(file, DictDecoder.USE_WRITABLE_BYTEBUFFER);
+            } else {
+                throw new RuntimeException("DictUpdater for version " + formatVersion + " doesn't"
+                        + " exist.");
+            }
+            dictUpdater.deleteWord(word);
         } catch (IOException e) {
         } catch (UnsupportedFormatException e) {
         }
@@ -246,7 +242,7 @@
         }
     }
 
-    public void testInsertWord() {
+    private void runTestInsertWord(final int formatVersion) {
         File file = null;
         try {
             file = File.createTempFile("testInsertWord", TEST_DICT_FILE_EXTENSION,
@@ -270,33 +266,37 @@
         }
 
         MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD, getWordPosition(file, "abcd"));
-        insertAndCheckWord(file, "abcde", 10, false, null, null);
+        insertAndCheckWord(file, "abcde", 10, false, null, null, formatVersion);
 
-        insertAndCheckWord(file, "abcdefghijklmn", 10, false, null, null);
+        insertAndCheckWord(file, "abcdefghijklmn", 10, false, null, null, formatVersion);
         checkReverseLookup(file, "abcdefghijklmn", getWordPosition(file, "abcdefghijklmn"));
 
-        insertAndCheckWord(file, "abcdabcd", 10, false, null, null);
+        insertAndCheckWord(file, "abcdabcd", 10, false, null, null, formatVersion);
         checkReverseLookup(file, "abcdabcd", getWordPosition(file, "abcdabcd"));
 
         // update the existing word.
-        insertAndCheckWord(file, "abcdabcd", 15, true, null, null);
+        insertAndCheckWord(file, "abcdabcd", 15, true, null, null, formatVersion);
 
         // split 1
-        insertAndCheckWord(file, "ab", 20, false, null, null);
+        insertAndCheckWord(file, "ab", 20, false, null, null, formatVersion);
 
         // split 2
-        insertAndCheckWord(file, "ami", 30, false, null, null);
+        insertAndCheckWord(file, "ami", 30, false, null, null, formatVersion);
 
-        deleteWord(file, "ami");
+        deleteWord(file, "ami", formatVersion);
         assertEquals(FormatSpec.NOT_VALID_WORD, getWordPosition(file, "ami"));
 
-        insertAndCheckWord(file, "abcdabfg", 30, false, null, null);
+        insertAndCheckWord(file, "abcdabfg", 30, false, null, null, formatVersion);
 
-        deleteWord(file, "abcd");
+        deleteWord(file, "abcd", formatVersion);
         assertEquals(FormatSpec.NOT_VALID_WORD, getWordPosition(file, "abcd"));
     }
 
-    public void testInsertWordWithBigrams() {
+    public void testInsertWord() {
+        runTestInsertWord(VERSION3);
+    }
+
+    private void runTestInsertWordWithBigrams(final int formatVersion) {
         File file = null;
         try {
             file = File.createTempFile("testInsertWordWithBigrams", TEST_DICT_FILE_EXTENSION,
@@ -323,8 +323,8 @@
         final ArrayList<WeightedString> banana = new ArrayList<WeightedString>();
         banana.add(new WeightedString("banana", 10));
 
-        insertAndCheckWord(file, "banana", 0, false, null, null);
-        insertAndCheckWord(file, "recursive", 60, true, banana, null);
+        insertAndCheckWord(file, "banana", 0, false, null, null, formatVersion);
+        insertAndCheckWord(file, "recursive", 60, true, banana, null, formatVersion);
 
         final PtNodeInfo info = findWordFromFile(file, "recursive");
         int bananaPos = getWordPosition(file, "banana");
@@ -333,7 +333,11 @@
         assertEquals(info.mBigrams.get(0).mAddress, bananaPos);
     }
 
-    public void testRandomWords() {
+    public void testInsertWordWithBigrams() {
+        runTestInsertWordWithBigrams(VERSION3);
+    }
+
+    private void runTestRandomWords(final int formatVersion) {
         File file = null;
         try {
             file = File.createTempFile("testRandomWord", TEST_DICT_FILE_EXTENSION,
@@ -362,7 +366,7 @@
         int cnt = 0;
         for (final String word : sWords) {
             final long diff = insertAndCheckWord(file, word,
-                    cnt % FormatSpec.MAX_TERMINAL_FREQUENCY, false, null, null);
+                    cnt % FormatSpec.MAX_TERMINAL_FREQUENCY, false, null, null, formatVersion);
             maxTimeToInsert = Math.max(maxTimeToInsert, diff);
             minTimeToInsert = Math.min(minTimeToInsert, diff);
             sum += diff;
@@ -373,8 +377,13 @@
             MoreAsserts.assertNotEqual(FormatSpec.NOT_VALID_WORD, getWordPosition(file, word));
         }
 
+        Log.d(TAG, "Test version " + formatVersion);
         Log.d(TAG, "max = " + ((double)maxTimeToInsert/1000000) + " ms.");
         Log.d(TAG, "min = " + ((double)minTimeToInsert/1000000) + " ms.");
         Log.d(TAG, "avg = " + ((double)sum/mMaxUnigrams/1000000) + " ms.");
     }
+
+    public void testRandomWords() {
+        runTestRandomWords(VERSION3);
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java b/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java
index 36b958a..a270ee7 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/CodePointUtils.java
@@ -24,6 +24,42 @@
         // This utility class is not publicly instantiable.
     }
 
+    public static final int[] LATIN_ALPHABETS_LOWER = {
+        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+        0x00E0 /* LATIN SMALL LETTER A WITH GRAVE */,
+        0x00E1 /* LATIN SMALL LETTER A WITH ACUTE */,
+        0x00E2 /* LATIN SMALL LETTER A WITH CIRCUMFLEX */,
+        0x00E3 /* LATIN SMALL LETTER A WITH TILDE */,
+        0x00E4 /* LATIN SMALL LETTER A WITH DIAERESIS */,
+        0x00E5 /* LATIN SMALL LETTER A WITH RING ABOVE */,
+        0x00E6 /* LATIN SMALL LETTER AE */,
+        0x00E7 /* LATIN SMALL LETTER C WITH CEDILLA */,
+        0x00E8 /* LATIN SMALL LETTER E WITH GRAVE */,
+        0x00E9 /* LATIN SMALL LETTER E WITH ACUTE */,
+        0x00EA /* LATIN SMALL LETTER E WITH CIRCUMFLEX */,
+        0x00EB /* LATIN SMALL LETTER E WITH DIAERESIS */,
+        0x00EC /* LATIN SMALL LETTER I WITH GRAVE */,
+        0x00ED /* LATIN SMALL LETTER I WITH ACUTE */,
+        0x00EE /* LATIN SMALL LETTER I WITH CIRCUMFLEX */,
+        0x00EF /* LATIN SMALL LETTER I WITH DIAERESIS */,
+        0x00F0 /* LATIN SMALL LETTER ETH */,
+        0x00F1 /* LATIN SMALL LETTER N WITH TILDE */,
+        0x00F2 /* LATIN SMALL LETTER O WITH GRAVE */,
+        0x00F3 /* LATIN SMALL LETTER O WITH ACUTE */,
+        0x00F4 /* LATIN SMALL LETTER O WITH CIRCUMFLEX */,
+        0x00F5 /* LATIN SMALL LETTER O WITH TILDE */,
+        0x00F6 /* LATIN SMALL LETTER O WITH DIAERESIS */,
+        0x00F7 /* LATIN SMALL LETTER O WITH STROKE */,
+        0x00F9 /* LATIN SMALL LETTER U WITH GRAVE */,
+        0x00FA /* LATIN SMALL LETTER U WITH ACUTE */,
+        0x00FB /* LATIN SMALL LETTER U WITH CIRCUMFLEX */,
+        0x00FC /* LATIN SMALL LETTER U WITH DIAERESIS */,
+        0x00FD /* LATIN SMALL LETTER Y WITH ACUTE */,
+        0x00FE /* LATIN SMALL LETTER THORN */,
+        0x00FF /* LATIN SMALL LETTER Y WITH DIAERESIS */
+    };
+
     public static int[] generateCodePointSet(final int codePointSetSize, final Random random) {
         final int[] codePointSet = new int[codePointSetSize];
         for (int i = codePointSet.length - 1; i >= 0; ) {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/SparseTableTests.java b/tests/src/com/android/inputmethod/latin/makedict/SparseTableTests.java
index 132483d..aeb8552 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/SparseTableTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/SparseTableTests.java
@@ -21,10 +21,8 @@
 import android.util.Log;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Random;
@@ -36,9 +34,6 @@
 public class SparseTableTests extends AndroidTestCase {
     private static final String TAG = SparseTableTests.class.getSimpleName();
 
-    private static final int[] SMALL_INDEX = { SparseTable.NOT_EXIST, 0 };
-    private static final int[] BIG_INDEX = { SparseTable.NOT_EXIST, 1, 2, 3, 4, 5, 6, 7};
-
     private final Random mRandom;
     private final ArrayList<Integer> mRandomIndex;
 
@@ -59,32 +54,21 @@
         }
     }
 
-    public void testInitializeWithArray() {
-        final SparseTable table = new SparseTable(SMALL_INDEX, BIG_INDEX, BLOCK_SIZE);
-        for (int i = 0; i < 8; ++i) {
-            assertEquals(SparseTable.NOT_EXIST, table.get(i));
-        }
-        assertEquals(SparseTable.NOT_EXIST, table.get(8));
-        for (int i = 9; i < 16; ++i) {
-            assertEquals(i - 8, table.get(i));
-        }
-    }
-
     public void testSet() {
-        final SparseTable table = new SparseTable(16, BLOCK_SIZE);
-        table.set(3, 6);
-        table.set(8, 16);
+        final SparseTable table = new SparseTable(16, BLOCK_SIZE, 1);
+        table.set(0, 3, 6);
+        table.set(0, 8, 16);
         for (int i = 0; i < 16; ++i) {
             if (i == 3 || i == 8) {
-                assertEquals(i * 2, table.get(i));
+                assertEquals(i * 2, table.get(0, i));
             } else {
-                assertEquals(SparseTable.NOT_EXIST, table.get(i));
+                assertEquals(SparseTable.NOT_EXIST, table.get(0, i));
             }
         }
     }
 
     private void generateRandomIndex(final int size, final int prop) {
-        for (int i = 0; i < DEFAULT_SIZE; ++i) {
+        for (int i = 0; i < size; ++i) {
             if (mRandom.nextInt(100) < prop) {
                 mRandomIndex.set(i, mRandom.nextInt());
             } else {
@@ -94,11 +78,11 @@
     }
 
     private void runTestRandomSet() {
-        final SparseTable table = new SparseTable(DEFAULT_SIZE, BLOCK_SIZE);
+        final SparseTable table = new SparseTable(DEFAULT_SIZE, BLOCK_SIZE, 1);
         int elementCount = 0;
         for (int i = 0; i < DEFAULT_SIZE; ++i) {
             if (mRandomIndex.get(i) != SparseTable.NOT_EXIST) {
-                table.set(i, mRandomIndex.get(i));
+                table.set(0, i, mRandomIndex.get(i));
                 elementCount++;
             }
         }
@@ -107,29 +91,24 @@
               + table.getContentTableSize());
         Log.d(TAG, "the table has " + elementCount + " elements");
         for (int i = 0; i < DEFAULT_SIZE; ++i) {
-            assertEquals(table.get(i), (int)mRandomIndex.get(i));
+            assertEquals(table.get(0, i), (int)mRandomIndex.get(i));
         }
 
         // flush and reload
         OutputStream lookupOutStream = null;
         OutputStream contentOutStream = null;
-        InputStream lookupInStream = null;
-        InputStream contentInStream = null;
         try {
             final File lookupIndexFile = File.createTempFile("testRandomSet", ".small");
             final File contentFile = File.createTempFile("testRandomSet", ".big");
             lookupOutStream = new FileOutputStream(lookupIndexFile);
             contentOutStream = new FileOutputStream(contentFile);
-            table.write(lookupOutStream, contentOutStream);
-            lookupInStream = new FileInputStream(lookupIndexFile);
-            contentInStream = new FileInputStream(contentFile);
-            final byte[] lookupArray = new byte[(int) lookupIndexFile.length()];
-            final byte[] contentArray = new byte[(int) contentFile.length()];
-            lookupInStream.read(lookupArray);
-            contentInStream.read(contentArray);
-            final SparseTable newTable = new SparseTable(lookupArray, contentArray, BLOCK_SIZE);
+            table.write(lookupOutStream, new OutputStream[] { contentOutStream });
+            lookupOutStream.flush();
+            contentOutStream.flush();
+            final SparseTable newTable = SparseTable.readFromFiles(lookupIndexFile,
+                    new File[] { contentFile }, BLOCK_SIZE);
             for (int i = 0; i < DEFAULT_SIZE; ++i) {
-                assertEquals(table.get(i), newTable.get(i));
+                assertEquals(table.get(0, i), newTable.get(0, i));
             }
         } catch (IOException e) {
             Log.d(TAG, "IOException while flushing and realoding", e);
@@ -157,4 +136,60 @@
             runTestRandomSet();
         }
     }
+
+    public void testMultipleContents() {
+        final int numOfContents = 5;
+        generateRandomIndex(DEFAULT_SIZE, 20);
+        final SparseTable table = new SparseTable(DEFAULT_SIZE, BLOCK_SIZE, numOfContents);
+        for (int i = 0; i < mRandomIndex.size(); ++i) {
+            if (mRandomIndex.get(i) != SparseTable.NOT_EXIST) {
+                for (int j = 0; j < numOfContents; ++j) {
+                    table.set(j, i, mRandomIndex.get(i));
+                }
+            }
+        }
+
+        OutputStream lookupOutStream = null;
+        OutputStream[] contentsOutStream = new OutputStream[numOfContents];
+        try {
+            final File lookupIndexFile = File.createTempFile("testMultipleContents", "small");
+            lookupOutStream = new FileOutputStream(lookupIndexFile);
+            final File[] contentFiles = new File[numOfContents];
+            for (int i = 0; i < numOfContents; ++i) {
+                contentFiles[i] = File.createTempFile("testMultipleContents", "big" + i);
+                contentsOutStream[i] = new FileOutputStream(contentFiles[i]);
+            }
+            table.write(lookupOutStream, contentsOutStream);
+            lookupOutStream.flush();
+            for (int i = 0; i < numOfContents; ++i) {
+                contentsOutStream[i].flush();
+            }
+            final SparseTable newTable = SparseTable.readFromFiles(lookupIndexFile, contentFiles,
+                    BLOCK_SIZE);
+            for (int i = 0; i < numOfContents; ++i) {
+                for (int j = 0; j < DEFAULT_SIZE; ++j) {
+                    assertEquals(table.get(i, j), newTable.get(i, j));
+                }
+            }
+        } catch (IOException e) {
+            Log.d(TAG, "IOException while flushing and reloading", e);
+        } finally {
+            if (lookupOutStream != null) {
+                try {
+                    lookupOutStream.close();
+                } catch (IOException e) {
+                    Log.d(TAG, "IOException while closing the stream", e);
+                }
+            }
+            for (int i = 0; i < numOfContents; ++i) {
+                if (contentsOutStream[i] != null) {
+                    try {
+                        contentsOutStream[i].close();
+                    } catch (IOException e) {
+                        Log.d(TAG, "IOException while closing the stream.", e);
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index ddc9546..7c1decb 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -109,35 +109,54 @@
         dict.close();
     }
 
+    /**
+     * Clear all entries in the user history dictionary.
+     * @param testFilenameSuffix file name suffix used for testing.
+     */
+    private void clearHistory(final String testFilenameSuffix) {
+        final UserHistoryDictionary dict =
+                PersonalizationHelper.getUserHistoryDictionary(getContext(),
+                        testFilenameSuffix /* locale */, mPrefs);
+        dict.clearAndFlushDictionary();
+        dict.close();
+    }
+
+    /**
+     * Shut down executer and wait until all operations of user history are done.
+     * @param testFilenameSuffix file name suffix used for testing.
+     */
+    private void waitForWriting(final String testFilenameSuffix) {
+        try {
+            final UserHistoryDictionary dict =
+                    PersonalizationHelper.getUserHistoryDictionary(getContext(),
+                            testFilenameSuffix, mPrefs);
+            dict.shutdownExecutorForTests();
+            while (!dict.isTerminatedForTests()) {
+                Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
+            }
+        } catch (InterruptedException e) {
+            Log.d(TAG, "InterruptedException: ", e);
+        }
+    }
+
     public void testRandomWords() {
-        File dictFile = null;
         Log.d(TAG, "This test can be used for profiling.");
         Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
         final String testFilenameSuffix = "testRandomWords" + System.currentTimeMillis();
+        final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix
+                + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
+
         final int numberOfWords = 1000;
         final Random random = new Random(123456);
 
         try {
+            clearHistory(testFilenameSuffix);
             addAndWriteRandomWords(testFilenameSuffix, numberOfWords, random,
                     true /* checksContents */);
         } finally {
-            try {
-                final UserHistoryDictionary dict =
-                        PersonalizationHelper.getUserHistoryDictionary(getContext(),
-                                testFilenameSuffix, mPrefs);
-                Log.d(TAG, "waiting for writing ...");
-                dict.shutdownExecutorForTests();
-                while (!dict.isTerminatedForTests()) {
-                    Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
-                }
-            } catch (InterruptedException e) {
-                Log.d(TAG, "InterruptedException: " + e);
-            }
-
-            final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix
-                    + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
-            dictFile = new File(getContext().getFilesDir(), fileName);
-
+            Log.d(TAG, "waiting for writing ...");
+            waitForWriting(testFilenameSuffix);
+            final File dictFile = new File(getContext().getFilesDir(), fileName);
             if (dictFile != null) {
                 assertTrue(dictFile.exists());
                 assertTrue(dictFile.length() >= MIN_USER_HISTORY_DICTIONARY_FILE_SIZE);
@@ -162,6 +181,7 @@
                 final String fileName = UserHistoryDictionary.NAME + "." +
                         testFilenameSuffixes[i] + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
                 dictFiles[i] = new File(getContext().getFilesDir(), fileName);
+                clearHistory(testFilenameSuffixes[i]);
             }
 
             final long start = System.currentTimeMillis();
@@ -178,19 +198,9 @@
             Log.d(TAG, "testStressTestForSwitchingLanguageAndAddingWords took "
                     + (end - start) + " ms");
         } finally {
-            try {
-                Log.d(TAG, "waiting for writing ...");
-                for (int i = 0; i < numberOfLanguages; i++) {
-                    final UserHistoryDictionary dict =
-                            PersonalizationHelper.getUserHistoryDictionary(getContext(),
-                                    testFilenameSuffixes[i], mPrefs);
-                    dict.shutdownExecutorForTests();
-                    while (!dict.isTerminatedForTests()) {
-                        Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
-                    }
-                }
-            } catch (InterruptedException e) {
-                Log.d(TAG, "InterruptedException: " + e);
+            Log.d(TAG, "waiting for writing ...");
+            for (int i = 0; i < numberOfLanguages; i++) {
+                waitForWriting(testFilenameSuffixes[i]);
             }
             for (final File file : dictFiles) {
                 if (file != null) {
@@ -203,33 +213,21 @@
     }
 
     public void testAddManyWords() {
-        File dictFile = null;
         final String testFilenameSuffix = "testRandomWords" + System.currentTimeMillis();
         final int numberOfWords =
                 ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ?
                         10000 : 1000;
         final Random random = new Random(123456);
-
-        UserHistoryDictionary dict =
-                PersonalizationHelper.getUserHistoryDictionary(getContext(),
-                        testFilenameSuffix, mPrefs);
+        clearHistory(testFilenameSuffix);
         try {
             addAndWriteRandomWords(testFilenameSuffix, numberOfWords, random,
                     true /* checksContents */);
-            dict.close();
         } finally {
-            try {
-                Log.d(TAG, "waiting for writing ...");
-                dict.shutdownExecutorForTests();
-                while (!dict.isTerminatedForTests()) {
-                    Thread.sleep(WAIT_TERMINATING_IN_MILLISECONDS);
-                }
-            } catch (InterruptedException e) {
-                Log.d(TAG, "InterruptedException: ", e);
-            }
+            Log.d(TAG, "waiting for writing ...");
+            waitForWriting(testFilenameSuffix);
             final String fileName = UserHistoryDictionary.NAME + "." + testFilenameSuffix
                     + ExpandableBinaryDictionary.DICT_FILE_EXTENSION;
-            dictFile = new File(getContext().getFilesDir(), fileName);
+            final File dictFile = new File(getContext().getFilesDir(), fileName);
             if (dictFile != null) {
                 assertTrue(dictFile.exists());
                 assertTrue(dictFile.length() >= MIN_USER_HISTORY_DICTIONARY_FILE_SIZE);
diff --git a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
index cf3bdd6..1fd5c98 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.utils;
 
+import com.android.inputmethod.latin.settings.SettingsValues;
+
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
@@ -25,64 +27,64 @@
 @SmallTest
 public class CapsModeUtilsTests extends AndroidTestCase {
     private static void onePathForCaps(final CharSequence cs, final int expectedResult,
-            final int mask, final Locale l, final boolean hasSpaceBefore) {
+            final int mask, final SettingsValues sv, final boolean hasSpaceBefore) {
         int oneTimeResult = expectedResult & mask;
         assertEquals("After >" + cs + "<", oneTimeResult,
-                CapsModeUtils.getCapsMode(cs, mask, l, hasSpaceBefore));
+                CapsModeUtils.getCapsMode(cs, mask, sv, hasSpaceBefore));
     }
 
     private static void allPathsForCaps(final CharSequence cs, final int expectedResult,
-            final Locale l, final boolean hasSpaceBefore) {
+            final SettingsValues sv, final boolean hasSpaceBefore) {
         final int c = TextUtils.CAP_MODE_CHARACTERS;
         final int w = TextUtils.CAP_MODE_WORDS;
         final int s = TextUtils.CAP_MODE_SENTENCES;
-        onePathForCaps(cs, expectedResult, c | w | s, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, w | s, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, c | s, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, c | w, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, c, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, w, l, hasSpaceBefore);
-        onePathForCaps(cs, expectedResult, s, l, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c | w | s, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, w | s, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c | s, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c | w, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, c, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, w, sv, hasSpaceBefore);
+        onePathForCaps(cs, expectedResult, s, sv, hasSpaceBefore);
     }
 
     public void testGetCapsMode() {
         final int c = TextUtils.CAP_MODE_CHARACTERS;
         final int w = TextUtils.CAP_MODE_WORDS;
         final int s = TextUtils.CAP_MODE_SENTENCES;
-        Locale l = Locale.ENGLISH;
-        allPathsForCaps("", c | w | s, l, false);
-        allPathsForCaps("Word", c, l, false);
-        allPathsForCaps("Word.", c, l, false);
-        allPathsForCaps("Word ", c | w, l, false);
-        allPathsForCaps("Word. ", c | w | s, l, false);
-        allPathsForCaps("Word..", c, l, false);
-        allPathsForCaps("Word.. ", c | w | s, l, false);
-        allPathsForCaps("Word... ", c | w | s, l, false);
-        allPathsForCaps("Word ... ", c | w | s, l, false);
-        allPathsForCaps("Word . ", c | w, l, false);
-        allPathsForCaps("In the U.S ", c | w, l, false);
-        allPathsForCaps("In the U.S. ", c | w, l, false);
-        allPathsForCaps("Some stuff (e.g. ", c | w, l, false);
-        allPathsForCaps("In the U.S.. ", c | w | s, l, false);
-        allPathsForCaps("\"Word.\" ", c | w | s, l, false);
-        allPathsForCaps("\"Word\". ", c | w | s, l, false);
-        allPathsForCaps("\"Word\" ", c | w, l, false);
+        SettingsValues sv = SettingsValues.makeDummySettingsValuesForTest(Locale.ENGLISH);
+        allPathsForCaps("", c | w | s, sv, false);
+        allPathsForCaps("Word", c, sv, false);
+        allPathsForCaps("Word.", c, sv, false);
+        allPathsForCaps("Word ", c | w, sv, false);
+        allPathsForCaps("Word. ", c | w | s, sv, false);
+        allPathsForCaps("Word..", c, sv, false);
+        allPathsForCaps("Word.. ", c | w | s, sv, false);
+        allPathsForCaps("Word... ", c | w | s, sv, false);
+        allPathsForCaps("Word ... ", c | w | s, sv, false);
+        allPathsForCaps("Word . ", c | w, sv, false);
+        allPathsForCaps("In the U.S ", c | w, sv, false);
+        allPathsForCaps("In the U.S. ", c | w, sv, false);
+        allPathsForCaps("Some stuff (e.g. ", c | w, sv, false);
+        allPathsForCaps("In the U.S.. ", c | w | s, sv, false);
+        allPathsForCaps("\"Word.\" ", c | w | s, sv, false);
+        allPathsForCaps("\"Word\". ", c | w | s, sv, false);
+        allPathsForCaps("\"Word\" ", c | w, sv, false);
 
         // Test for phantom space
-        allPathsForCaps("Word", c | w, l, true);
-        allPathsForCaps("Word.", c | w | s, l, true);
+        allPathsForCaps("Word", c | w, sv, true);
+        allPathsForCaps("Word.", c | w | s, sv, true);
 
         // Tests after some whitespace
-        allPathsForCaps("Word\n", c | w | s, l, false);
-        allPathsForCaps("Word\n", c | w | s, l, true);
-        allPathsForCaps("Word\n ", c | w | s, l, true);
-        allPathsForCaps("Word.\n", c | w | s, l, false);
-        allPathsForCaps("Word.\n", c | w | s, l, true);
-        allPathsForCaps("Word.\n ", c | w | s, l, true);
+        allPathsForCaps("Word\n", c | w | s, sv, false);
+        allPathsForCaps("Word\n", c | w | s, sv, true);
+        allPathsForCaps("Word\n ", c | w | s, sv, true);
+        allPathsForCaps("Word.\n", c | w | s, sv, false);
+        allPathsForCaps("Word.\n", c | w | s, sv, true);
+        allPathsForCaps("Word.\n ", c | w | s, sv, true);
 
-        l = Locale.FRENCH;
-        allPathsForCaps("\"Word.\" ", c | w, l, false);
-        allPathsForCaps("\"Word\". ", c | w | s, l, false);
-        allPathsForCaps("\"Word\" ", c | w, l, false);
+        sv = SettingsValues.makeDummySettingsValuesForTest(Locale.FRENCH);
+        allPathsForCaps("\"Word.\" ", c | w, sv, false);
+        allPathsForCaps("\"Word\". ", c | w | s, sv, false);
+        allPathsForCaps("\"Word\" ", c | w, sv, false);
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
index 3eabe2b..1944fd3 100644
--- a/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtilsTests.java
@@ -196,8 +196,8 @@
         final UserHistoryDictionaryBigramList resultList = new UserHistoryDictionaryBigramList();
         final OnAddWordListener listener = new OnAddWordListener() {
             @Override
-            public void setUnigram(final String word,
-                    final String shortcutTarget, final int frequency) {
+            public void setUnigram(final String word, final String shortcutTarget,
+                    final int frequency, final int shortcutFreq) {
                 Log.d(TAG, "in: setUnigram: " + word + "," + frequency);
                 resultList.addBigram(null, word, (byte)frequency);
             }
@@ -220,8 +220,8 @@
         final UserHistoryDictionaryBigramList resultList2 = new UserHistoryDictionaryBigramList();
         final OnAddWordListener listener2 = new OnAddWordListener() {
             @Override
-            public void setUnigram(final String word,
-                    final String shortcutTarget, final int frequency) {
+            public void setUnigram(final String word, final String shortcutTarget,
+                    final int frequency, final int shortcutFreq) {
                 Log.d(TAG, "in: setUnigram: " + word + "," + frequency);
                 resultList2.addBigram(null, word, (byte)frequency);
             }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
index bd06e9f..e571bc2 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
@@ -183,7 +183,11 @@
                             filename + " does not seem to be a dictionary file"));
                 } else if (CombinedInputOutput.isCombinedDictionary(
                         decodedSpec.mFile.getAbsolutePath())){
-                    if (report) System.out.println("Format : Combined format");
+                    if (report) {
+                        System.out.println("Format : Combined format");
+                        System.out.println("Packaging : " + decodedSpec.describeChain());
+                        System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
+                    }
                     return CombinedInputOutput.readDictionaryCombined(
                             new BufferedInputStream(new FileInputStream(decodedSpec.mFile)));
                 } else {
diff --git a/tools/make-keyboard-text/res/values-ca/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-ca/donottranslate-more-keys.xml
index 9728c99..4cf7424 100644
--- a/tools/make-keyboard-text/res/values-ca/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-ca/donottranslate-more-keys.xml
@@ -72,7 +72,7 @@
     <string name="more_keys_for_l">l&#x00B7;l,&#x0142;</string>
     <!-- U+00B7: "·" MIDDLE DOT -->
     <string name="more_keys_for_punctuation">"!fixedColumnOrder!4,&#x00B7;,!,\\,,\?,:,;,\@"</string>
-    <string name="more_keys_for_tablet_period">\?,&#x00B7;</string>
+    <string name="more_keys_for_period">\?,&#x00B7;</string>
     <!-- U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA -->
     <string name="keylabel_for_spanish_row2_10">&#x00E7;</string>
 </resources>
diff --git a/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
index 8494296..8e6b4ee 100644
--- a/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-es/donottranslate-more-keys.xml
@@ -75,7 +75,7 @@
     <!-- U+00A1: "¡" INVERTED EXCLAMATION MARK -->
     <string name="more_keys_for_tablet_comma">"!,&#x00A1;"</string>
     <!-- U+00BF: "¿" INVERTED QUESTION MARK -->
-    <string name="more_keys_for_tablet_period">"\?,&#x00BF;"</string>
+    <string name="more_keys_for_period">"\?,&#x00BF;"</string>
     <string name="keylabel_for_apostrophe">\"</string>
     <string name="keyhintlabel_for_apostrophe">\'</string>
     <string name="more_keys_for_apostrophe">\'</string>
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 5a03c80..ab4fbda 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
@@ -81,8 +81,8 @@
     <string name="keylabel_for_tablet_comma">"&#x060C;"</string>
     <string name="keyhintlabel_for_tablet_comma">"!"</string>
     <string name="more_keys_for_tablet_comma">"!,\\,"</string>
-    <string name="keyhintlabel_for_tablet_period">"&#x061F;"</string>
-    <string name="more_keys_for_tablet_period">"&#x061F;,\?"</string>
+    <string name="keyhintlabel_for_period">"&#x061F;"</string>
+    <string name="more_keys_for_period">"&#x061F;,\?"</string>
     <string name="keylabel_for_apostrophe">&#x060C;</string>
     <string name="keyhintlabel_for_apostrophe">&#x061F;</string>
     <string name="more_keys_for_apostrophe">"!fixedColumnOrder!4,:,!,&#x061F;,&#x061B;,-,/,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;"</string>
diff --git a/tools/make-keyboard-text/res/values-iw/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-iw/donottranslate-more-keys.xml
index feaed4c..a163331 100644
--- a/tools/make-keyboard-text/res/values-iw/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-iw/donottranslate-more-keys.xml
@@ -55,6 +55,6 @@
     <string name="keylabel_for_currency">&#x20AA;</string>
     <string name="keyhintlabel_for_tablet_comma">!</string>
     <string name="more_keys_for_tablet_comma">!</string>
-    <string name="keyhintlabel_for_tablet_period">\?</string>
-    <string name="more_keys_for_tablet_period">\?</string>
+    <string name="keyhintlabel_for_period">\?</string>
+    <string name="more_keys_for_period">\?</string>
 </resources>
diff --git a/tools/make-keyboard-text/res/values-sv/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values-sv/donottranslate-more-keys.xml
index a36a13e..2472364 100644
--- a/tools/make-keyboard-text/res/values-sv/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values-sv/donottranslate-more-keys.xml
@@ -18,39 +18,77 @@
 */
 -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+         U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+         U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+         U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+         U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE -->
+    <string name="more_keys_for_a">&#x00E1;,&#x00E0;,&#x00E2;,&#x0105;,&#x00E3;</string>
+    <!-- U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+         U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+         U+010D: "č" LATIN SMALL LETTER C WITH CARON -->
+    <string name="more_keys_for_c">&#x00E7;,&#x0107;,&#x010D;</string>
+    <!-- U+00F0: "ð" LATIN SMALL LETTER ETH
+         U+010F: "ď" LATIN SMALL LETTER D WITH CARON -->
+    <string name="more_keys_for_d">&#x00F0;,&#x010F;</string>
     <!-- U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
          U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
          U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
          U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
          U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK -->
     <string name="more_keys_for_e">&#x00E9;,&#x00E8;,&#x00EA;,&#x00EB;,&#x0119;</string>
-    <!-- U+0153: "œ" LATIN SMALL LIGATURE OE
-         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+    <!-- U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+         U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+         U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+         U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS -->
+    <string name="more_keys_for_i">&#x00ED;,&#x00EC;,&#x00EE;,&#x00EF;</string>
+    <!-- U+0142: "ł" LATIN SMALL LETTER L WITH STROKE -->
+    <string name="more_keys_for_l">&#x0142;</string>
+    <!-- U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+         U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+         U+0148: "ň" LATIN SMALL LETTER N WITH CARON -->
+    <string name="more_keys_for_n">&#x0144;,&#x00F1;,&#x0148;</string>
+    <!-- U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
          U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
-         U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+         U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
          U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
          U+014D: "ō" LATIN SMALL LETTER O WITH MACRON -->
-    <string name="more_keys_for_o">&#x0153;,&#x00F4;,&#x00F2;,&#x00F3;,&#x00F5;,&#x014D;</string>
+    <string name="more_keys_for_o">&#x00F3;,&#x00F2;,&#x00F4;,&#x00F5;,&#x014D;</string>
+    <!-- U+0159: "ř" LATIN SMALL LETTER R WITH CARON -->
+    <string name="more_keys_for_r">&#x0159;</string>
+    <!-- U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+         U+0161: "š" LATIN SMALL LETTER S WITH CARON
+         U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+         U+00DF: "ß" LATIN SMALL LETTER SHARP S -->
+    <string name="more_keys_for_s">&#x015B;,&#x0161;,&#x015F;,&#x00DF;</string>
+    <!-- U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+         U+00FE: "þ" LATIN SMALL LETTER THORN -->
+    <string name="more_keys_for_t">&#x0165;,&#x00FE;</string>
     <!-- U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
-         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
-         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
          U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+         U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+         U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
          U+016B: "ū" LATIN SMALL LETTER U WITH MACRON -->
-    <string name="more_keys_for_u">&#x00FC;,&#x00FB;,&#x00F9;,&#x00FA;,&#x016B;</string>
-    <!-- U+00DF: "ß" LATIN SMALL LETTER SHARP S
-         U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
-         U+0161: "š" LATIN SMALL LETTER S WITH CARON -->
-    <string name="more_keys_for_s">&#x00DF;,&#x015B;,&#x0161;</string>
+    <string name="more_keys_for_u">&#x00FC;,&#x00FA;,&#x00F9;,&#x00FB;,&#x016B;</string>
+    <!-- U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+         U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+         U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS -->
+    <string name="more_keys_for_y">&#x00FD;,&#x00FF;,&#x00FC;</string>
+    <!-- U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+         U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+         U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE -->
+    <string name="more_keys_for_z">&#x017A;,&#x017E;,&#x017C;</string>
     <!-- U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE -->
     <string name="keylabel_for_nordic_row1_11">&#x00E5;</string>
-    <!-- U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS -->
-    <string name="keylabel_for_nordic_row2_10">&#x00F6;</string>
     <!-- U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS -->
     <string name="keylabel_for_nordic_row2_11">&#x00E4;</string>
-    <!-- U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE -->
-    <string name="more_keys_for_nordic_row2_10">&#x00F8;</string>
     <!-- U+00E6: "æ" LATIN SMALL LETTER AE -->
     <string name="more_keys_for_nordic_row2_11">&#x00E6;</string>
+    <!-- U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS -->
+    <string name="keylabel_for_nordic_row2_10">&#x00F6;</string>
+    <!-- U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+         U+0153: "œ" LATIN SMALL LIGATURE OE -->
+    <string name="more_keys_for_nordic_row2_10">&#x00F8;,&#x0153;</string>
     <string name="single_angle_quotes">!text/single_raqm_laqm</string>
     <string name="double_angle_quotes">!text/double_raqm_laqm</string>
 </resources>
diff --git a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
index cc09f7f..39ecfab 100644
--- a/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
+++ b/tools/make-keyboard-text/res/values/donottranslate-more-keys.xml
@@ -169,9 +169,9 @@
     <string name="keylabel_for_tablet_comma">,</string>
     <string name="keyhintlabel_for_tablet_comma"></string>
     <string name="more_keys_for_tablet_comma"></string>
-    <string name="keyhintlabel_for_tablet_period"></string>
+    <string name="keyhintlabel_for_period"></string>
     <!-- U+2026: "…" HORIZONTAL ELLIPSIS -->
-    <string name="more_keys_for_tablet_period">&#x2026;</string>
+    <string name="more_keys_for_period">&#x2026;</string>
     <string name="keylabel_for_apostrophe">\'</string>
     <string name="keyhintlabel_for_apostrophe">\"</string>
     <string name="more_keys_for_apostrophe">\"</string>
@@ -246,4 +246,5 @@
     <string name="more_keys_for_single_quote">!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes</string>
     <string name="more_keys_for_double_quote">!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes</string>
     <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes</string>
+    <string name="emoji_key_as_more_key">!icon/emoji_key|!code/key_emoji</string>
 </resources>